树状数组

树状数组

Screen Shot 2020-10-05 at 10.42.16 AM

前置知识: lowbit 函数

lowbit函数用于求一个非负整数n在二进制表示下最低位1及其后面的0所构成的数值

Decimal Binary Lowbit(Binary) Lowbit(Decimal)
1 001 1 1
3 011 1 1
4 100 100 4
8 1000 1000 8

对于一个数6,要求其lowbit,可以先将其按位取反再加1,再与原数按位与

\[\begin{align} &110 \\ &010 (按位取反再加1) \\ &010 (按位与: lowbit(6) ) \end{align} \]

在计算机中非负整数的取反加1就是这个数的相反数,因此可以O(1)求出lowbit(x)

inline int lowbit(int x){
  return x & -x;
}

树状数组

树状数组的基本思想是,每一个结点x都只存储包含x的前lowbit(x)的元素的区间和.

这样构建的数据结构如下图:

Screen Shot 2020-10-05 at 10.42.16 AM

前缀和查询

这张图将lowbit相同的结点置于同一层.由图可以看出,当求结点7的前缀和时,需要经历如下步骤

- 求7后(包含7)的前lowbit(7)个元素,即求[7,7]区间和
- 求6后(包含6)的前lowbit(6)个元素,即求[5,6]区间和
- 求4后(包含4)的前lowbit(4)个元素,即求[1,4]区间和
上述求解方法,实际上是将[1,7]区间和转化成[1,4];[5,6];[7]的区间和之

易知,若x为2的幂,则lowbit(x) = x,因此,采用树状数组来建树,对于长度为n的数列,所构建的树状数组的树高为 log(n).

因此可知,查询前缀和操作的最坏复杂度为log(n)

inline int query(int x){
  int ans = 0;
  while(x >= 1){
    ans += c[x];
    x = x - lowbit(x);
  }
  return ans;
}

单点修改

对于树状数组,修改操作是查询操作的逆过程.

Screen Shot 2020-10-05 at 10.42.16 AM

对于这个图,可以看出

  • 第n层元素的lowbit均为\(2^{n-1}\)
  • 第n层元素之间的差值为\(2^{n}\),刚好为第n+1层元素的lowbit

因此,修改位置x的值,直接影响到的只有上一层的x+lowbit(x).即对于上一层的元素只有一个元素需要修改.

inline void add(int x,int val,int n){
  while(x <= n){
    c[x] += val;
    x = x + lowbit(x);
  }
}

同样,单点修改操作的最坏复杂度为log(n)

完整代码

#include <cstdio>

using namespace std;

#define N  500000 + 5

int c[N];

inline int read(){
    int x = 0;
    int flag = 0;
    char c = getchar();
    while(c<'0'||c>'9'){
        if(c == '-'){
            flag = 1;
        }
        c = getchar();
    }
    
    while(c>='0'&&c<='9'){
        x = (x<<1)+(x<<3)+(c^48);
        c = getchar();
    }
    
    if(flag){
        return -x;
    }else{
        return x;
    }
}

inline int lowbit(int x){
    return x & -x;
}

inline void add(int n,int pos,int val){
    while(pos <= n){
        c[pos] += val;
        pos = pos + lowbit(pos);
    }
}

inline int query(int pos){
    int ans = 0;
    while(pos >= 1){
        ans += c[pos];
        pos = pos - lowbit(pos);
    }
    return ans;
}


int main(){
    int n,m;
    int cmd,x,y;
    n = read();
    m = read();
    
    for(int i = 1; i <= n; i++){
        x = read();
        add(n,i,x);
    }
    
    while(m--){
        cmd = read();
        x = read();
        y = read();
        
        if(cmd == 1){
            add(n,x,y);
        }else{
            printf("%d\n",query(y)-query(x-1));
        }
    }
    
    return 0;
}


变式

区间修改,单点查询

朴素的树状数组能够做到单点修改,区间查询.本质上,这样的树状数组是利用: 存储前缀,查询差分的思想.

即维护前缀和数组,利用前缀和数组的差分来查询区间和.

换一种思路,我们还可以维护差分数组,利用差分数组的前缀和来查询单点,即

// 原数组  a[1],a[2],a[3],...,a[n]
// 差分数组 b[i] = a[i] - a[i-1] 			(预处理)
// 差分前缀和 b[1] + ... + b[x] = a[0] + ... + a[x] = a[x] 
#include <cstdio>

using namespace std;

#define N  500000 + 5

int a[N];
int c[N];

inline int read(){
    int x = 0;
    int flag = 0;
    char c = getchar();
    while(c<'0'||c>'9'){
        if(c == '-'){
            flag = 1;
        }
        c = getchar();
    }
    
    while(c>='0'&&c<='9'){
        x = (x<<1)+(x<<3)+(c^48);
        c = getchar();
    }
    
    if(flag){
        return -x;
    }else{
        return x;
    }
}

inline int lowbit(int x){
    return x & -x;
}

inline void add(int n,int pos,int val){
    while(pos <= n){
        c[pos] += val;
        pos = pos + lowbit(pos);
    }
}

inline int query(int pos){
    int ans = 0;
    while(pos >= 1){
        ans += c[pos];
        pos = pos - lowbit(pos);
    }
    return ans;
}


int main(){
    int n,m;
    int cmd,x,y,val;
    n = read();
    m = read();

    
    for(int i = 1; i <= n; i++){
        a[i] = read();
        add(n,i,a[i]-a[i-1]);
    }
    
    while(m--){
        cmd = read();
        if(cmd == 1){
            x = read();
            y = read();
            val = read();
            
            add(n,x,val);
            add(n,y+1,-val);
        }else{
            x = read();
            printf("%d\n",query(x));
        }
    }
    
    return 0;
}


求逆序对

链接:树状数组求逆序对

#include <iostream>
#include <algorithm>

using namespace std;

#define N 100000 + 5

struct node{
    int val;
    int pos;
    node(){};
    node(int x,int y){
        val = x;
        pos = y;
    }
};

inline bool comp(const node&a,const node&b){
    return a.val <= b.val;
}

int c[N]; // c[i] 实时记录了数值在[1,i]区间的数的总数
int b[N]; // 离散化处理
node arr[N]; // 原数组

inline int lowbit(int x){
    return x & -x;
}

inline void add(int n,int pos,int val){ // 单点修改
    while (pos <= n) {
        c[pos] += val;
        pos = pos + lowbit(pos);
    }
}

inline int query(int pos){ // 查询c[pos]即查询当前一共出现的数值在[1,pos]的数的总数
    int ans = 0;
    while(pos >= 1){
        ans += c[pos];
        pos = pos - lowbit(pos);
    }
    return ans;
}

int main(){
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++){
        cin >> arr[i].val;
        arr[i].pos = i;
    }
    /* 排序,求出每一个元素应该处在的位置 */
    sort(arr+1,arr+1+n,comp);
    
    /* 离散化处理,将复杂度由MAXN将至n */
    int cnt = 1;
    b[arr[1].pos] = 1;
    for(int i = 2; i <= n; i++){
        if(arr[i].val != arr[i-1].val){
            cnt++;
        }
        b[arr[i].pos] = cnt;
    }
    
    int ans = 0;
    for(int i = 1; i <= n; i++){ // 第i个加入的数
        add(n,b[i],1);
        ans += (i - query(b[i])); // 第i个加入的数 - 当前出现的不大于(当前加入的数)的数的总数 = 逆序数
    }
    cout << ans << endl;
    return 0;
}

posted @ 2020-10-24 10:13  popozyl  阅读(115)  评论(0编辑  收藏  举报