本文中或許會引進部分圖片來自網絡,但大多數內容均為原創qwq。
樹狀數組或者二叉索引樹也稱作Binary Indexed Tree,又叫做Fenwick樹。
它的查詢和修改的時間復雜度都是log(n)
,空間復雜度則為O(n).
(這也是我們為什么使用樹狀數組的原因)
樹狀數組可以將線性結構轉化成樹狀結構,從而進行跳躍式掃描,通常使用在高效的計算數列的前綴和,區間和,同時,我們在運用線段樹的時應先考慮是不是可以使用樹狀數組來解決問題。
也就是說,在解題思路中,樹狀數組的優先度是大於線段樹的(當然對於神仙們是除外的)
這同樣適用於我們針對於排名rank的排序,不過這個時候就需要建立結構體式的樹狀數組了(我是這么叫的qwq)
下面開始從0入門了:
1.單點查詢
我們先從數組講起(這個就不需要普及了吧);
A數組是我們傳入數據的數組
C數組使我們建立起來的樹狀數組
我們通過這里可以顯而易見地發現這樣一個規律:
C1 = A1 C2 = A1+A2 C3 = A3 C4 = A1+A2+A3+A4 C5 = A5 C6 = A5+A6 C7 = A7 C8 = A1+A2+A3+A4+A5+A6+A7+A8
請大家好好理解上述代碼,這是樹狀數組的基礎
接下來我們引入lowbit這個概念:(這個地方有一點需要注意:lowbit(0)會陷入死循環 )
inline int lowbit(int x) { return x & (-x); }
這返回的是這個數字最高位的1;
在這之前,又要引入一個補碼的概念:
補碼的表示方法是:
正數的補碼就是其本身
負數的補碼是在其原碼的基礎上, 符號位不變, 其余各位取反, 最后+1. (即在反碼的基礎上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]補
[-1] = [10000001]原 = [11111110]反 = [11111111]補
請注意,這里的第一位是指的是符號位,而不是數字位(這是1,因此數字位只有1)
對於負數, 補碼表示方式也是人腦無法直觀看出其數值的. 通常也需要轉換成原碼在計算其數值.
因此,&是求與的一個符號,意思是 a 和 b 同時為 1 的時候返回這個最高位(不包括符號位)
在剛剛的找規律過程中,我們通過規律總結出了以下性質(lowbit是為了幫助程序代碼的實現)
我們可以得到樹狀數組的一些性質:對於c[i],他的兒子節點取決於i的所有因子中最多有2^j次冪,則向前取2^j個數作為兒子,即[i-2^j+1,i]。(這個時候就需要lowbit來幫助實現)
舉一個栗子:
6的最大2次方因子為2,即2^1,則向前取2個數,則c[6]=a[5]+a[6];
8的最大2次方因子為8,即2^3,則向前取8個數,則c[8]=a[1]+a[2]+...+a[8]。
2.單點修改
當我們要對最底層的值進行更新時,那么它相應的父親節點存儲的和也需要進行更新,
我們建立的樹狀數組結構是一個完整的結構,因此修改一個點也會需要所有相應的其父親節點的點來修改,這樣我們就實現了樹狀數組的修改。
代碼如下:
void modify(int x,int k) //將 x 增加 k { if(x < 1) return ; while(x <= n) { c[i] += k; x += lowbit(x); //去尋找它的父親 } }
3.單點查詢
單點查詢由於我們向前統計,因此需要向前查找,這個就不需要講了吧(沒弄明白請看上面)
int query(int pos) { int sum=0; for(int i=pos;i;i-=lowbit(i)) sum += c[pos]; /*兩種寫法 while(pos > 0) { sum += c[pos]; pos -= lowbit(pos); } */
return sum; }
至此為止,我們已經講完了樹狀數組的基礎內容。
貼一下基礎部分的代碼:
void change(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 query(int l, int r) { //區間求和 return ask(r) - ask(l - 1); }
請確保在以上內容均熟練掌握的情況下再學習以下知識點。
在進入接下來的學習中,建議先做一下這幾個題
以上這些只是我們學習樹狀數組的基礎,真正的高端樹狀數組是可以在很大的范圍內進行局部優化和大幅度降低復雜度的
舉一個小栗子:
當我們在面對很大的數據范圍的時候,就可以先離散化,再針對其進行樹狀數組的一個對應關系
放心,這只是我們面對於數據組的優化,而既然是樹狀數組,便肯定不會受限於這一些東西;
接下來開始正題:
4.區間修改與單點查詢
對於這種問題的思路就是對於題目所給出的區間進行差分的操作。
如果不知道差分的同學請補習之后再來
查詢:
設原數組為a[i];
設數組c[i] = a[i]−a[i−1](a[0]=0)
c[i] = a[i]−a[i−1](a[0]=0);
則 a[i]= ∑i j=1 c[j]
可以通過求c[i]的前綴和查詢。
修改:
當給區間[l,r]加上x的時候;
a[l]與前一個元素a[l−1] 的差增加了x;a[r+1]與 a[r]的差減少了x。
根據c[i]數組的定義,只需給a[l]加上x, 給a[r+1]減去x即可。
Codes:
void add(int p, int x) { while(p <= n) { c[p] += x; p += lowbit (p); } } void range_add(int l, int r, int x) { add(l, x); add(r + 1, -x); } int ask(int p) { int res = 0; while(p) { res += sum[p],; p += lowbit (p); } return res; }
5.區間修改與區間查詢
這個地方是我們在線段樹中的重點與難點(lazytag),但是如今我們有了樹狀數組,於是便可以有另一種方法來解決它了;
怎么求呢?我們基於問題2的“差分”思路,考慮一下如何在問題2構建的樹狀數組中求前綴和:
位置p的前綴和如下:
∑p i=1 a[i] = ∑ p i=1 ∑ i j=1 c[j]
哇,寫這個式子真的難受
當我們發現在這里
c[1]被使用了p次;
c[2]被使用了p-1次;
~~~~
我們就可以針對於這個式子進行改進:
∑ p i=1 ∑ i j=1 c[j] = ∑p i=1 c[i] ∗ (p − i + 1) = (p + 1) ∗ ∑ p i=1 c[i] − ∑ p i=1 c[i] ∗ i
這樣我們便可以建立兩個數組進行維護前綴和:
sum1[i]=d[i];
sum2[i]=d[i]∗i
查詢:
p位置的前綴和便是在sum1中(p + 1)的前綴和減去sum2中p位置的前綴和
那么區間[l,r]的前綴和就是r的前綴和減去l的前綴和
修改:
因為我們對於sum1的修改同於問題二中的修改
對於sum2的修改是對於sum2[l] 加上 l * x,給 sum2[r + 1] 減去 (r + 1) * x。
用這個做區間修改區間求和的題,無論是時間上還是空間上都比帶lazytag的線段樹要優。
(這也是為什么樹狀數組的初步價值)
Codes:
#define ll long long void add(ll p, ll x) { for(int i = p; i <= n; i += lowbit(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 -= lowbit(i)) res += (p + 1) * sum1[i] - sum2[i]; //重點 return res; } ll range_ask(ll l, ll r) { return ask(r) - ask(l - 1); }
6.二維樹狀數組
我們已經學會了對於序列的常用操作,那么對於矩陣呢,還記得這個夢么(蒟蒻我看不懂什么神仙題)
能不能把類似的操作應用到矩陣上呢?這時候我們就要寫二維樹狀數組了!
在一維樹狀數組中,c[x]記錄的是右端點為x、長度為lowbit(x)的區間的區間和。
那么在二維樹狀數組中,可以類似地定義c[x][y]記錄的是右下角為(x, y),高為lowbit(x), 寬為 lowbit(y)的區間的區間和。
好的,很好qwq,這個地方的操作實際上是類似於一維的;
而且理解起來也不是很難,看着代碼也許會好一些吧q
單點修改與區間查詢:
void add(int x, int y, int z) //將點(x, y)加上z { int lasty = y; while(x <= n) { y = lasty; while(y <= n) //因為是修改,所以一直到(n,n)都要修改 { tree[x][y] += z; y += lowbit(y); } x += lowbit (x); } } void ask(int x, int y) //求左上角為(1,1)右下角為(x,y) 的矩陣和 { int res = 0 int lasty = y; while(x) { y = lasty; while(y) { res += tree[x][y]; y -= lowbit(y); } x -= lowbit(x); } }
區間修改和單點查詢
這個需要用的二維數組的前綴和
二維的前綴和就差不多長這樣
其實二維的前綴和在實現的時候還是有不少的困難的,但是這並不是我們在今天所主要涉及的內容,
如果對於二維數組的前綴和不是很理解請戳這里或者上網自行百度
Codes:
void add(int x, int y, int z) { int lasty = y; while(x <= n) { y = lasty; while(y <= n) { tree[x][y] += z; y += lowbit(y); } x += lowbit(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 = 0l; int lasty = y; while(x) { y = lasty; while(y) { res += tree[x][y]; y -= y & -y; } x -= lowbit(x); } }
這個遠遠不是想象中的那樣難,只是相當於對於一個二維數組進行了壓縮。
對於二維數組里的內容起到了一個區域求值的方法,這也是樹狀數組的核心所在
區間修改和區間查詢
(截圖markdown真好用!)
這個式子一般就是我們在面對二維數組求區間和的問題時候的究極無敵暴力策略吧...
顯然是可以卡回祖宗的
這個時候再找一下規律,我們又會發現:
d[1][1]出現了x∗y次
d[1][2]出現了x∗(y−1)次……
d[h][k]出現了 (x−h+1)∗(y−k+1)次。
這說明了(找規律大法好)我們可以對於這個進行樹狀數組優化:
我們對於這個式子進行多項式運算,就會有以下的這些過程
這樣我們就只需要開四個樹狀數組,分別維護四個變量就足夠了
sum1[]維護 c[i][j]
sum2[]維護 c[i][j] * i
sum3[]維護 c[i][j] * j
sum4[]維護 c[i][j] * i * j
就完成了操作了!
貼一個簡單點的代碼:
Codes1:
#define ll long long #define RI register int int n,m,last,opt,x,y,z,mian,opt; int sum1[500002],sum2[500002]; int lowbit(int x) { return x & (-x); } void in(int &x) int f = 1; x = 0; char ch = getchar(); while(ch > '9' || ch < '0') { if(s == '-') f = -1; ch = getchar(); } while(ch <= '9' && ch >= '0') { x = x * 10 + s - '0'; ch = getchar(); } x *= f; } void add(int pos,int x) { for(RI i=pos;i<=n;i+=lowbit(i)) sum1[i]+=x,sum2[i]+=pos*x; } ll query(int pos) { long long res=0; for(RI i=pos;i;i-=lowbit(i)) res += (pos + 1) * sum1[i] - sum2[i]; return res; } int main() { in(n); in(m); for(RI i=1;i<=n;i++) { in(x); add(i,x-last); last=x; } for(RI i=1;i<=m;i++) { in(opt); switch(opt) { case 1:in(x),in(y),in(z),add(x,z),add(y+1,-z);break; case 2:in(z),mian+=z;break; case 3:in(z),mian-=z;break; case 4: { in(x),in(y); if (x == 1) printf("%lld\n",query(y) - query(x - 1) + mian); else printf("%lld\n",query(y) - query(x - 1)) break; } case 5:printf("%lld\n",query(1) + mian); } } return 0; }
Codes2:(代碼搬磚自胡小兔)
#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; }
好了。
完結撒花,其實還有一些知識點,想起來再更新吧。
碼量驚人,客官點個推薦吧qwq