本文中或許會引進部分圖片來自網絡,但大多數內容均爲原創qwq。ios
樹狀數組或者二叉索引樹也稱做Binary Indexed Tree,又叫作Fenwick樹。數組
它的查詢和修改的時間複雜度都是log(n)
,空間複雜度則爲O(n).
markdown
(這也是咱們爲何使用樹狀數組的緣由)網絡
樹狀數組能夠將線性結構轉化成樹狀結構,從而進行跳躍式掃描,一般使用在高效的計算數列的前綴和,區間和,同時,咱們在運用線段樹的時應先考慮是否是可使用樹狀數組來解決問題。學習
也就是說,在解題思路中,樹狀數組的優先度是大於線段樹的(固然對於神仙們是除外的)優化
這一樣適用於咱們針對於排名rank的排序,不過這個時候就須要創建結構體式的樹狀數組了(我是這麼叫的qwq)atom
下面開始從0入門了:spa
1.單點查詢.net
咱們先從數組講起(這個就不須要普及了吧);code
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