• 树状数组


    树状数组

    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,再与原数按位与

    [egin{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
    ",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
    ",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;
    }
    
    
    ---- suffer now and live the rest of your life as a champion ----
  • 相关阅读:
    ubuntu下解决无法解析或打开软件包列表或状态文件的问题
    linux 解除文件root权限限制
    查看linux设备信息的命令
    R系安装rpm包
    重启窗口管理器
    内存泄漏如何定位?
    双屏显示,HDMI可以正常显示,lvds不显示
    避免linux下log在/var/log/messages 中重复输出的办法
    debian编包成功后,想要修改的文件的内容没有变化
    linux terminal 显示不全 将log内容打印出来
  • 原文地址:https://www.cnblogs.com/popodynasty/p/13867743.html
Copyright © 2020-2023  润新知