zoukankan      html  css  js  c++  java
  • 高级树状数组——区间修改区间查询、二维树状数组

    “高级”数据结构——树状数组!

    ※本文一切代码未经编译,不保证正确性,如发现问题,欢迎指正!

    1. 单点修改 + 区间查询

    最简单的树状数组就是这样的:

    void add(int p, int x){ //给位置p增加x
        while(p <= n) sum[p] += x, p += p & -p;
    }
    int ask(int p){ //求位置p的前缀和
        int res = 0;
        while(p) res += sum[p], p -= p & -p;
        return res;
    }
    int range_ask(int l, int r){ //区间求和
        return ask(r) - ask(l - 1);
    }
    

    2. 区间修改 + 单点查询

    通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。

    查询

    设原数组为(a[i]), 设数组(d[i] = a[i] - a[i - 1] (a[0] = 0)),则 (a[i] = sum_{j = 1}^{i}d[j]),可以通过求(d[i])的前缀和查询。

    修改

    当给区间([l, r])加上x的时候,(a[l]) 与前一个元素 (a[l - 1]) 的差增加了(x)(a[r + 1])(a[r]) 的差减少了(x)。根据(d[i])数组的定义,只需给(d[l]) 加上 (x), 给(d[r + 1]) 减去 (x) 即可。

    void add(int p, int x){ //这个函数用来在树状数组中直接修改
        while(p <= n) sum[p] += x, p += p & -p;
    }
    void range_add(int l, int r, int x){ //给区间[l, r]加上x
        add(l, x), add(r + 1, -x);
    }
    int ask(int p){ //单点查询
        int res = 0;
        while(p) res += sum[p], p -= p & -p;
        return res;
    }
    

    3. 区间修改 + 区间查询

    这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组!

    怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:

    位置p的前缀和 =

    [sum_{i = 1}^{p} a[i] = sum_{i = 1}^{p} sum_{j = 1}^{i} d[j] ]

    在等式最右侧的式子(sum_{i = 1}^{p} sum_{j = 1}^{i} d[j])中,(d[1]) 被用了(p)次,(d[2])被用了(p - 1)次……那么我们可以写出:

    位置p的前缀和 =

    [sum_{i = 1}^{p} sum_{j = 1}^{i} d[j] = sum_{i = 1}^{p} d[i] * (p - i + 1) = (p + 1) * sum_{i = 1}^{p}d[i] - sum_{i = 1}^{p}d[i]*i ]

    那么我们可以维护两个数组的前缀和:
    一个数组是 (sum1[i] = d[i])
    另一个数组是 (sum2[i] = d[i] * i)

    查询

    位置p的前缀和即: (p + 1) * sum1数组中p的前缀和 - sum2数组中p的前缀和。

    区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。

    修改

    对于sum1数组的修改同问题2中对d数组的修改。

    对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。

    void add(ll p, ll x){
        for(int i = p; i <= n; i += i & -i)
            sum1[i] += x, sum2[i] += x * p;
    }
    void range_add(ll l, ll r, ll x){
        add(l, x), add(r + 1, -x);
    }
    ll ask(ll p){
        ll res = 0;
        for(int i = p; i; i -= i & -i)
            res += (p + 1) * sum1[i] - sum2[i];
        return res;
    }
    ll range_ask(ll l, ll r){
        return ask(r) - ask(l - 1);
    }
    

    用这个做区间修改区间求和的题,无论是时间上还是空间上都比带lazy标记的线段树要优。

    4. 二维树状数组

    我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊喂)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了!

    在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
    那么在二维树状数组中,可以类似地定义tree[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。

    单点修改 + 区间查询

    void add(int x, int y, int z){ //将点(x, y)加上z
        int memo_y = y;
        while(x <= n){
            y = memo_y;
            while(y <= n)
                tree[x][y] += z, y += y & -y;
            x += x & -x;
        }
    }
    void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和
        int res = 0, memo_y = y;
        while(x){
            y = memo_y;
            while(y)
                res += tree[x][y], y -= y & -y;
            x -= x & -x;
        }
    }
    

    区间修改 + 单点查询

    我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。

    那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。

    二维前缀和:

    [sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j] ]

    那么我们可以令差分数组(d[i][j]) 表示 (a[i][j])(a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1]) 的差。

    例如下面这个矩阵

     1  4  8
     6  7  2
     3  9  5
    

    对应的差分数组就是

     1  3  4
     5 -2 -9
    -3  5  1
    

    当我们想要将一个矩阵加上x时,怎么做呢?
    下面是给最中间的3*3矩阵加上x时,差分数组的变化:

    0  0  0  0  0
    0 +x  0  0 -x
    0  0  0  0  0
    0  0  0  0  0
    0 -x  0  0 +x
    

    这样给修改差分,造成的效果就是:

    0  0  0  0  0
    0  x  x  x  0
    0  x  x  x  0
    0  x  x  x  0
    0  0  0  0  0
    

    那么我们开始写代码吧!

    void add(int x, int y, int z){ 
        int memo_y = y;
        while(x <= n){
            y = memo_y;
            while(y <= n)
                tree[x][y] += z, y += y & -y;
            x += x & -x;
        }
    }
    void range_add(int xa, int ya, int xb, int yb, int z){
        add(xa, ya, z);
        add(xa, yb + 1, -z);
        add(xb + 1, ya, -z);
        add(xb + 1, yb + 1, z);
    }
    void ask(int x, int y){
        int res = 0, memo_y = y;
        while(x){
            y = memo_y;
            while(y)
                res += tree[x][y], y -= y & -y;
            x -= x & -x;
        }
    }
    

    区间修改 + 区间查询

    类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和:

    [sum_{i=1}^{x}sum_{j=1}^{y}sum_{k=1}^{i}sum_{h=1}^{j}d[h][k]$$ (d[h][k]为点(h, k)对应的“二维差分”(同上题)) 这个式子炒鸡复杂( $O(n^4)$ 复杂度!),但利用树状数组,我们可以把它优化到 $O(log^2n)$! 首先,类比一维数组,统计一下每个$d[h][k]$出现过多少次。$d[1][1]$出现了$x*y$次,$d[1][2]$出现了$x*(y - 1)$次……$d[h][k]$ 出现了 $(x - h + 1)*(y - k + 1)$ 次。 那么这个式子就可以写成: $$sum_{i=1}^{x}sum_{j=1}^{y}d[i][j] * (x + 1 - i) * (y + 1 - j)]

    把这个式子展开,就得到:

    [(x + 1) * (y + 1) * sum_{i=1}^{x}sum_{j=1}^{y}d[i][j] ]

    [- (y + 1) * sum_{i=1}^{x}sum_{j=1}^{y}d[i][j] * i ]

    [- (x + 1) * sum_{i=1}^{x}sum_{j=1}^{y}d[i][j] * j ]

    [+ sum_{i=1}^{x}sum_{j=1}^{y}d[i][j] * i * j ]

    那么我们要开四个树状数组,分别维护:

    (d[i][j], d[i][j] * i, d[i][j] * j, d[i][j] * i * j)

    这样就完成了!

    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <algorithm>
    #include <iostream>
    using namespace std;
    typedef long long ll;
    ll read(){
        char c; bool op = 0;
        while((c = getchar()) < '0' || c > '9')
            if(c == '-') op = 1;
        ll res = c - '0';
        while((c = getchar()) >= '0' && c <= '9')
            res = res * 10 + c - '0';
        return op ? -res : res;
    }
    const int N = 205;
    ll n, m, Q;
    ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];
    void add(ll x, ll y, ll z){
        for(int X = x; X <= n; X += X & -X)
            for(int Y = y; Y <= m; Y += Y & -Y){
                t1[X][Y] += z;
                t2[X][Y] += z * x;
                t3[X][Y] += z * y;
                t4[X][Y] += z * x * y;
            }
    }
    void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形
        add(xa, ya, z);
        add(xa, yb + 1, -z);
        add(xb + 1, ya, -z);
        add(xb + 1, yb + 1, z);
    }
    ll ask(ll x, ll y){
        ll res = 0;
        for(int i = x; i; i -= i & -i)
            for(int j = y; j; j -= j & -j)
                res += (x + 1) * (y + 1) * t1[i][j]
                    - (y + 1) * t2[i][j]
                    - (x + 1) * t3[i][j]
                    + t4[i][j];
        return res;
    }
    ll range_ask(ll xa, ll ya, ll xb, ll yb){
        return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
    }
    int main(){
        n = read(), m = read(), Q = read();
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                ll z = read();
                range_add(i, j, i, j, z);
            }
        }
        while(Q--){
            ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();
            if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))
                range_add(xa, ya, xb, yb, a);
        }
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++)
                printf("%lld ", range_ask(i, j, i, j));
            putchar('
    ');
        }
        return 0;
    }
    
  • 相关阅读:
    NodeJS实例系列~环境搭建,Hello world归来!
    Node.js教程系列~目录
    poj 1743 男人八题之后缀数组求最长不可重叠最长重复子串
    利用手工编码的方式对srtus2进行输入验证
    介绍linux下Source Insight强大代码编辑器sublime_text_3
    【机器学习】支持向量机[续1]
    boost库在工作(33)网络服务端之三
    HNCU1099:堆积木
    HNCU1100:彩票
    Lua获取网络时间
  • 原文地址:https://www.cnblogs.com/RabbitHu/p/BIT.html
Copyright © 2011-2022 走看看