【树形结构】树状数组

版权声明:来自达羌蒟蒻 https://blog.csdn.net/PHenning/article/details/89049107

树状数组!树状数组!

树状数组

真の树状数组


树状数组通常用于区间内的单点修改和求前缀和操作。
继树状数组之后,线段树、主席树、平衡树蜂拥而至,真叫人高兴

给定序列(数组) A A ,设数组 C C 满足 C [ i ] = A [ i 2 k + 1 ] + + A [ i ] C[i]=A[i–2^k+1]+\cdots+A[i] ,其中 k k i i 对应的二进制数末尾 0 0 的个数, i i 1 1 开始算,则称 C C 为树状数组。
2 k = l o w b i t ( i ) = i & i 2^k=lowbit(i)=i\&-i

当我们求 A [ 1 ] + + A [ x ] A[1]+\cdots+A[x] 的之和时, C [ x ] C[x] 如果包含的不是 1 x 1\dots x 的全部和,就需要再找一个 C [ k ] ( k < x ) C[k](k<x) 累加起来,其中 k = x l o w b i t ( x ) k=x-lowbit(x)

修改了某个 A [ x ] A[x] ,就需更改从叶子节点到根节点路径上的所有 C [ y ] C[y] ,其中 A [ x ] A[x] 的父亲编号 k = x + l o w b i t ( x ) k=x+lowbit(x)

时间复杂度: O ( log 2 n ) O(\log_2n)

struct indexed_tree{
  int c[maxn],n;
  void init(int n){memset(c,0,sizeof(c));this->n=n;}
  void modify(int x,int d){for(int i=x;i<=n;i+=i&-i)c[i]+=d;}
  int query(int x){
    int sum=0;
    for(int i=x;i>0;i-=i&-i)sum+=c[i];
    return sum;
  }
};

求逆序对

a [ i ] a[i] 记录当前数字 i i 出现的次数。对每一个数字 x x ,首先查询 [ x + 1 , n ] [x+1,n] 这段区间中的数字出现的个数,因为它们是在 x x 之前出现的,又比 x x 大,所以都能与 x x 构成逆序对;然后再把 x x 插入到树状数组中,更新所有包含了 x x c c

indexed_tree T;T.init(n);
int ans=0;
for(int i=1;i<=n;i++){
  ans+=T.query(n)-T.query(a[i]);
  T.modify(a[i],1);
}

走丢的奶牛(NKOJ 3709)

问题描述
约翰有 n n 头奶牛,这些奶牛长得很相似,约翰经常分不清谁是谁,于是约翰给他们编号 1 1 n n ,以此来区分每头奶牛。
今天奶牛们想作弄一下约翰, n n 头奶牛乱序排成一排。
约翰想要分清每个位置对应奶牛的编号。约翰从左起第 2 2 头奶牛开始一直到第 n n 头奶牛,依次问每个奶牛一个问题“你左边有多少个编号比你小的奶牛”,奶牛们都会如实回答约翰的问题。
问,你能否根据奶牛的回答,帮助约翰推出这 n n 头牛对应的编号?

输入格式
第一行,一个整数 n n
接下来 n 1 n-1 行,每行一个整数,分别表示左起第 2 2 到第 n n 头奶牛的回答。

输出格式
n n 行,每行一个整数,对应从左往右每头奶牛的编号

样例输入
5
1
2
1
0

样例输出
2
4
5
3
1

PS:NKOJ加强了数据, n 50000 n\leqslant 50000

原问题等价于:现有整数 1 1 n n 的某个排列,已知每一个数左边比这个数小的数的个数,求该排列。
把这个问题反过来,若已知原排列,则求每一个数左边比这个数小的数的个数就是用树状数组求顺序对的过程: c [ i ] c[i] 记目前数字 i i 出现的次数。每读入一个数 x x ,输出 c c 数组中前 x 1 x-1 项之和,然后把 c [ x ] c[x] 加一。
所以原题就是把这个过程反过来。先把 c c 数组全部设为 1 1 (因为最终 1 1 n n 每个数字各出现了一次),然后倒序读入,每读入一个数字 x x ,就需要在当前的 c c 数组中找到一个位置,使得这个位置左边有 x x 1 1 。那么这个位置就是 c c 数组从左往右第一个前缀和为 x + 1 x+1 的位置(保证这个位置一定为 1 1 )。该位置的下标即为当前位上的数字。然后把 c c 数组中这个位置的值减一。当讨论到第一个数字时, c c 数组中只有一个位置的值为 1 1 ,第一个数字就是这个位置的下标。
对每一位数字二分查找前缀和,时间复杂度 O ( n log 2 2 n ) O(n\log_2^2n)

const int maxn=50000+5;
int n,inp[maxn],ans[maxn];
struct indexed_tree{
  int c[maxn],n;
  void init(int n){memset(c,0,sizeof(c));this->n=n;}
  void modify(int x,int d){for(int i=x;i<=n;i+=i&-i)c[i]+=d;}
  int query(int x){
    int sum=0;
    for(int i=x;i>0;i-=i&-i)sum+=c[i];
    return sum;
  }
}T;
int bisect(int l,int r,int v){
  int ans;while(l<=r){
    int mid=l+r>>1;
    if(T.query(mid)>=v)ans=mid,r=mid-1;
    else l=mid+1;
  }
  return ans;
}
int main(){
  read(&n);T.init(n);
  for(int i=1;i<=n;i++)T.modify(i,1);
  for(int i=n;i>=2;i--)read(&inp[i]);
  for(int i=2;i<=n;i++){
    int a=bisect(1,n,inp[i]+1);
    ans[i-1]=a;T.modify(a,-1);
  }
  ans[n]=bisect(1,n,1);
  for(int i=n;i>=1;i--)write(ans[i],"\n");
  return 0;
}

区间修改的树状数组

区间修改需要用到差分。
这里数组 C C 定义为给定数组 A A 的差分数组,即 C [ i ] = A [ i ] A [ i 1 ] C[i]=A[i]-A[i-1]
修改区间 [ i , j ] [i,j] 的值只需修改 C [ i ] C[i] C [ j + 1 ] C[j+1] 两个位置。
根据差分数组的定义可知 A [ i ] = C [ 1 ] + C [ 2 ] + + C [ i ] A[i]=C[1]+C[2]+\cdots+C[i]

单点查询直接求 C C 数组的前缀和即可。
时间复杂度: O ( log 2 n ) O(\log_2n)

struct indexed_tree{
  int c[maxn],n;
  void init(int n){memset(c,0,sizeof(c));this->n=n;}
  void _modify(int x,int d){for(int i=x;i<=n;i+=i&-i)c[i]+=d;}
  void modify(int x,int y,int d){_modify(x,d);_modify(y+1,-d);}
  int query(int x){
    int sum=0;
    for(int i=x;i>0;i-=i&-i)sum+=c[i];
    return sum;
  }
};

若要求 A A 数组的前缀和,即求
A [ 1 ] + A [ 2 ] + + A [ i ] = ( C [ 1 ] ) + ( C [ 1 ] + C [ 2 ] ) + + j = 1 i C [ j ] = ( i ) C [ 1 ] + ( i 1 ) C [ 2 ] + + C [ i ] = i j = 1 i C [ j ] j = 1 i ( j 1 ) C [ j ] \begin{aligned}&amp;A[1]+A[2]+\cdots+A[i]\\ =&amp;(C[1])+(C[1]+C[2])+\cdots+\sum_{j=1}^iC[j]\\ =&amp;(i)C[1]+(i-1)C[2]+\cdots+C[i]\\ =&amp;i\sum_{j=1}^iC[j]-\sum_{j=1}^i(j-1)C[j] \end{aligned}

扫描二维码关注公众号,回复: 5908911 查看本文章

再定义一个 C 2 C_2 数组,满足 C 2 [ i ] = ( i 1 ) C [ i ] C_2[i]=(i-1)C[i] ,同时维护两个数组的前缀和,就可以实现区间查询了。
时间复杂度: O ( log 2 n ) O(\log_2n)

struct indexed_tree{
  int c[maxn],c2[maxn],n;
  void init(int n){
    memset(c,0,sizeof(c));
    memset(c2,0,sizeof(c2));
    this->n=n;
  }
  void _modify(int*arr,int x,int d){for(int i=x;i<=n;i+=i&-i)arr[i]+=d;}
  void modify(int x,int y,int d){
    _modify(c,x,d);_modify(c,y+1,-d);
    _modify(c2,x,(x-1)*d);_modify(c2,y+1,-y*d);
  }
  int _query(int*arr,int x){
    int sum=0;
    for(int i=x;i>0;i-=i&-i)sum+=arr[i];
    return sum;
  }
  int query(int x,int y){
    return y*_query(c,y)-_query(c2,y)-(x-1)*_query(c,x-1)+_query(c2,x-1);
  }
};

二维树状数组

二维树状数组(图太抽象画不出来QAQ)主要跟容斥原理混合使用。

struct indexed_tree{
  int c[maxn][maxn],n;
  void init(int n){memset(c,0,sizeof(c));this->n=n;}
  void modify(int x,int y,int d){
    for(int i=x;i<=n;i+=i&-i)
      for(int j=y;j<=n;j+=j&-j)c[i][j]+=d;
  }
  int query(int x,int y){
    int sum=0;
    for(int i=x;i>0;i-=i&-i)
      for(int j=y;j>0;j-=j&-j)sum+=c[i][j];
    return sum;
  }
};

猜你喜欢

转载自blog.csdn.net/PHenning/article/details/89049107