高級樹狀數組——區間修改區間查詢、二維樹狀數組

「高級」數據結構——樹狀數組!

※本文一切代碼未經編譯,不保證正確性,如發現問題,歡迎指正!ios

1. 單點修改 + 區間查詢

最簡單的樹狀數組就是這樣的:c++

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構建的樹狀數組中求前綴和:spa

位置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\)次……那麼咱們能夠寫出:code

位置p的前綴和 =get

\[\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('\n');
    }
    return 0;
}
相關文章
相關標籤/搜索