高階樹狀陣列——區間修改區間查詢、二維樹狀陣列
“高階”資料結構——樹狀陣列!
※本文一切程式碼未經編譯,不保證正確性,如發現問題,歡迎指正!
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]=∑ij=1d[j],可以通過求d[i]的字首和查詢。
修改
當給區間[l,r]
加上x的時候,a[l] 與前一個元素 a[l−1] 的差增加了x,a[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的字首和 =
在等式最右側的式子∑pi=1∑ij=1d[j]
中,d[1] 被用了p次,d[2]被用了p−1次……那麼我們可以寫出:
位置p的字首和 =
那麼我們可以維護兩個陣列的字首和:
一個數組是 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;
}
}
區間修改 + 單點查詢
我們對於一維陣列進行差分,是為了使差分陣列字首和等於原陣列對應位置的元素。
那麼如何對二維陣列進行差分呢?可以針對二維字首和的求法來設計方案。
二維字首和:
那麼我們可以令差分陣列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)的二維字首和:
∑i=1x∑j=1y∑k=1i∑h=1jd[h][k](d[h][k]為點(h, k)對應的“二維差分”(同上題))
這個式子炒雞複雜( O(n4)
複雜度!),但利用樹狀陣列,我們可以把它優化到 O(log2n)!
首先,類比一維陣列,統計一下每個d[h][k]
出現過多少次。d[1][1]出現了x∗y次,d[1][2]出現了x∗(y−1)次……d[h][k] 出現了 (x−h+1)∗(y−k+1)次。
那麼這個式子就可以寫成:
∑i=1x∑j=1yd[i][j]∗(x+1−i)∗(y+1