1. 程式人生 > >高階樹狀陣列——區間修改區間查詢、二維樹狀陣列

高階樹狀陣列——區間修改區間查詢、二維樹狀陣列

“高階”資料結構——樹狀陣列!

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

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[i1](a[0]=0),則 a[i]=ij=1d[j],可以通過求d[i]

的字首和查詢。

修改

當給區間[l,r]

加上x的時候,a[l] 與前一個元素 a[l1] 的差增加了xa[r+1]a[r] 的差減少了x。根據d[i]陣列的定義,只需給a[l] 加上 x, 給a[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的字首和 =

i=1pa[i]=i=1pj=1id[j]

在等式最右側的式子pi=1ij=1d[j]

中,d[1] 被用了p次,d[2]被用了p1

次……那麼我們可以寫出:

位置p的字首和 =

i=1pj=1id[j]=i=1pd[i](pi+1)=(p+1)i=1pd[i]i=1pd[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[i1][j]+sum[i][j1]sum[i1][j1]+a[i][j]

那麼我們可以令差分陣列d[i][j]

表示 a[i][j]a[i1][j]+a[i][j1]a[i1][j1]

的差。

例如下面這個矩陣

 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)的二維字首和:

i=1xj=1yk=1ih=1jd[h][k]

(d[h][k]為點(h, k)對應的“二維差分”(同上題))

這個式子炒雞複雜( O(n4)

複雜度!),但利用樹狀陣列,我們可以把它優化到 O(log2n)

首先,類比一維陣列,統計一下每個d[h][k]

出現過多少次。d[1][1]出現了xy次,d[1][2]出現了x(y1)次……d[h][k] 出現了 (xh+1)(yk+1)

次。

那麼這個式子就可以寫成:

i=1xj=1yd[i][j](x+1i)(y+1