树状数组详解

  • Post author:
  • Post category:其他

1.树状数组

树状数组能够高效处理【对一个数组不断修改并求其前缀和】的问题,其修改与查询操作的复杂度都是

O

(

log

n

)

O(\log{n})

O(logn)

1.1.定义

对于已经维护好的前缀和,如果需要修改,则需要付出

O

(

n

)

O(n)

O(n) 的代价

  • 比如更改数组中的一个数,则其之后的前缀和的值都需要进行修改

树状数组是一种维护前缀和的数据结构,可以实现

O

(

log

n

)

O(\log{n})

O(logn)查询一个前缀的和,

O

(

log

n

)

O(\log{n})

O(logn) 对原数列的一个位置进行修改

  • 与前缀和相同的是,树状数组使用与原数列大小相同的数组即可维护

  • 与前缀和不同的是,树状数组的一个位置 i 存储的是从 i 开始,(包括 i)向前

    t

    i

    t_i

    ti 个元素的和

    • t

      i

      t_i

      ti 就是最大的可以整除 i 的2的幂(

      2

      0

      ,

      2

      1

      ,

      2

      2

      2^0, 2^1,2^2

      20,21,22等)
      在这里插入图片描述

    • 将这些数转化成 2 进制更容易看出规律

      在这里插入图片描述

    • 因为所有的整数都可以写成二进制形式,比如写成

      x

      =

      2

      9

      +

      2

      5

      +

      2

      3

      x = 2^9 + 2^5 + 2^3

      x=29+25+23。我们找最大的可以整除 x 的2的幂,需要保证该数能够整除

      2

      9

      ,

      2

      5

      ,

      2

      3

      2^9, 2^5, 2^3

      29,25,23 中的每一个,因此,这个数一定是这些相加的2的幂中最小的一个。

      t

      i

      t_i

      ti 其实就是其二进制最低位的1,与其后的 0 组成的二进制数

1.2.lowbit(i)

从名字上,可以看出是用来找最低位的,也就是二进制最低位的 1 及其后面的 0 组成的数的十进制大小。

我们发现,找一个数二进制的最低位的1及其后的 0,可以通过如下方式来寻找

lowbit(i) = i & i 取反再 + 1

在这里插入图片描述

在计算机中,我们将负数,用其正数的补码(取反再加 1)形式来存储。这样的优点是利于计算机实现运算操作。

因此上面的式子可以写成 lowbit(i) = i & (-i)

1.3.树状数组的查询

对于指定位置 x,要查询 [1, x] 的前缀和。我们发现,区间 [1, x] 可以使用一些现有的区间来表示

  • 对于区间 [1, 11] ,可以使用 s[11] + s[10] + s[8] 来表示

我们定义树状数组为 s

对于一个 x

S

[

x

]

=

i

(

x

t

x

,

x

]

d

[

i

]

S[x]=\sum_{i \in\left(x-t_{x}, x\right]} d[i]

S[x]=i(xtx,x]d[i],若令

y

=

x

t

x

y = x – t_x

y=xtx,那么前一个紧邻的区间就可以使用

(

y

t

y

,

y

]

(y-t_y, y]

(yty,y] 来表示。就是

S

[

y

]

S[y]

S[y]。这样不断迭代,就能完整的表示 [1, x] 区间

由于每次执行

x

t

x

x – t_x

xtx 的操作都会使现在数字 2 进制中的 1 的数量减少 1(从低位向高维减少),因此,最坏情况下需要执行此操作

log

2

x

\log_2{x}

log2x,所以时间复杂度为

O

(

log

n

)

O(\log{n})

O(logn)

代码实现,查询的结果就是前缀和

int s[MAXN]
#define lb(x) (x & (-x))
int ask(int x){
    int res = 0;
    for(int i = x; i >= 1; i -= lb(i))
        res += s[i];
    return res;
}

如果需要查询一段区间 [l, r] 的和,使用 ask(r) - ask(l - 1)

1.4.树状数组的修改

对于指定位置 x,要将 d[x] 的值增加 v,只需要关心树状数组中哪些位置包含 d[x] 的值,依次进行修改即可

例如:把 d[5] 的值增加 v,则受到影响的点为 s[5], s[6], s[8] 等

树状数组的性质:

  • 若当前节点为 x,且令

    x

    +

    t

    x

    x + t_x

    x+tx 为其父节点,则树状数组将形成一个树形结构

  • 节点 x 记录区间

    (

    x

    t

    x

    ,

    x

    ]

    (x – t_x, x]

    (xtx,x] 的信息,其子节点记录的区间是 x 记录区间的子集,且子集之间不会相互覆盖

  • 节点 x 记录的区间为节点 y 记录区间的子集,当且仅当 y 是 节点 x 的祖先节点

    在这里插入图片描述

我们只需要访问 x 所有祖先节点即可

int s[MAXN], n;
#define lb(x) (x & (-x))
void upd(int x, int v){
    for(int i = x; i <= n; i += lb(i))
        s[i] += v;
}

2.逆序对问题

找逆序对(

a

i

>

a

j

a_i > a_j

ai>aj

i

<

j

i < j

i<j 的有序对)的个数,注意序列中可能有重复的数字

2.1.输入格式

第一行,一个数 n,表示序列中有 n 个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过

1

0

9

10^9

109

2.2.输出格式

输出序列中逆序对的数目。

2.3.说明/提示

对于

25

%

25\%

25% 的数据,

n

2500

n \leq 2500

n2500

对于

50

%

50\%

50% 的数据,

n

4

×

1

0

4

n \leq 4 \times 10^4。

n4×104

对于所有数据,

n

5

×

1

0

5

n \leq 5 \times 10^5

n5×105

2.4.思想

核心思想:假定问题数组为 a,我们从左向右遍历整个数组,当遍历到 a[i]

  • 将其加入到树状数组中,也就是执行 upd(a[i], 1) 的操作。
  • 然后查询 a[i] 作为逆序对中较大的那个数时,产生的逆序对的数量 i - ask(a[i])
    • 因为从左往右遍历,所以在前面的数,都会先进入树状数组中。ask(a[i])得到的值就是索引在前面,且小于等于 a[i] 的数的个数(每次加入比a[i]小的,都会使树状数组中a[i] 对应的值也增加 1)。i - ask(a[i]) 就是索引在前面,且比 a[i] 大的数的个数
  • 如果,数的个数比较少,但是数的范围比较大,我们为了节省内存,需要进行离散化操作
    • 比如 n < 5000,a[i] < 10^9
    • 我们没法按数组中数原本的大小,开一个

      1

      0

      9

      10^9

      109 的数组,我们关注的是大小关系,只需要知道一个数是第几大的就行了,而不用关注其真实值到底是多少

      • 如果有相同大小的数,我们需要保证出现在前面的数,其新数(第几大),小于等于后出现数的新数,否则会将两个相同的数当做逆序对。这可以通过先对 a[i] 排序,再对索引排序来实现

2.5.代码

#include<bits/stdc++.h>
using namespace std;
#define lb(x) (x & -x)
#define ll long long
int n;
int t[500005]; // 树状数组
int ranks[500005]; // 记录当前数是第几大的,相同的数会对应不同rank,只要保证先出现的 rank 小于后出现的就可以
ll tot = 0; // 逆序对数目

struct point{
    int val; // 记录值
    int index; // 记录在原数组中的索引
    bool operator < (point a) const{ // 先通过值排序,再通过索引排序
        if(a.val == val)
            return index < a.index;
        return val < a.val;
    }
};
point a[500005];

void upd(int x, int v){
    for(int i = x; i <= n; i += lb(i))
        t[i] += v;
}

ll ask(int x){
    ll sum = 0;
    for(int i = x; i >= 1; i -= lb(i))
        sum += t[i];
    return sum;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i].val);
        a[i].index = i;
    }
    sort(a + 1, a + 1 + n);
    for(int i = 1; i <= n; i++) // 遍历新数组
        ranks[a[i].index] = i; // 第 i 大的数是原数组中第 a[i].index 个数
    for(int i = 1; i <= n; i++){ // 遍历原数组
        upd(ranks[i], 1);
        tot += (i - ask(ranks[i]));
    }
    printf("%lld", tot);
}

版权声明:本文为qq_52852138原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。