淺談樹狀陣列(解析+模板)
也不知道是什麼時候開始,對於曾經學過的演算法都不太用了
遇到區間修改,區間最值就知道用線段樹,什麼樹狀陣列啊,st表啊都忘得差不多了
最近幾次模考被卡翻了,於是又想起這些老朋友
來填個坑
首先我們要明確一點,樹狀陣列只能維護求和,不能維護區間最值
樹狀陣列利用了分治的思想,層數為logn,所以查詢和修改都是logn,總複雜度為詢問次數m乘logn
也就是mlogn,最關鍵的是和線段樹比起來,常數要小得多,跑的飛快
而空間複雜度則是n的,只用開一維,還不用結構體
但是樹狀陣列的應用範圍也相對較小
通常分為兩種
(1)單點修改+取件查詢
(2)區間修改+單點查詢
具體為什麼,我們一會兒會說到
首先來張圖片吧
這是比較流行的一種圖
顯而易見,樹狀陣列是上面的C陣列,而下面的A則是全陣列,練出來的線代表每個節點的值是由那幾個點組成的
例如:C[4]=C[2]+C[3]+A[3]
而我們如何找到組成當前節點的每一個點呢,或者說如何找到當前點的父親呢
這就引出了我們今天的重中之重
lowbit函式
來看一下這個函式長什麼樣子
int lowbit(int x){return x&(-x);}
對的,只要壓一下行就只有一行的小小的函式,就是整個樹狀陣列的核心了
雖然短,但是蘊含的內容卻很不好理解,這個函式所求的是x化為二進位制之後從末尾開始一共有幾個零
x加上這個數之後,就得到了他的父親節點的下標
減去這個數之後,就得到了上一個與x的子樹不相交的根節點(因為建立是就是這樣定義的)
具體的原因與二進位制中的補碼有關,我們在這裡就不詳細說了,當個模板來背即可
例如上圖中,6+2=8 6-2=4
而這兩種不同的運算也就對應了樹狀陣列中的兩種操作
操作一:單點修改
首先我們可以知道當前要修改的點在原陣列中的下標i,同時知道要加上(減去)的值v
根據lowbit函式的定義我們可以知道,包含原陣列中的值的節點的下標不可能小於原陣列的下標
同時改變某個點的值只會對其父親有影響,所以理所應當的加上lowbit(i),直到根節點
對於經過的每個節點,將權值加上v
void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}
同樣是壓行之後只有一行
操作二:區間查詢
和樹狀陣列的含義有關,當前的樹狀陣列中存的是類似於字首和的東西
所以我們很難得到一段區間的值,但是我們可以知道從1到x的值
假設要查詢的區間為[x,y],我們可以得到a[1]+a[2]+……+a[x-1],也可以同理得1到y
做一下差會可以了
具體的實現流程就是從當前點開始,不斷減去lowit(i),知道節點1,將路徑上的每一個點的值累加
特別一題,樹狀陣列中的下標不能為0,否則lowbit函式就會炸掉
放一下操作程式碼(同樣很簡潔,這也是樹狀陣列的優點之一)
int solve(int i){ int sum=0; for(;i>=1;i-=lowbit(i)) sum+=c[i]; return sum; }
以上就是樹狀陣列的單點修改和區間查詢
來一道完整的題:樹狀陣列模板1
附上AC程式碼:
#include<iostream> #include<cmath> #include<cstdio> #include<cstdlib> #include<cstring> #include<string> #include<algorithm> using namespace std; inline int min(int a,int b){return a<b?a:b;} inline int max(int a,int b){return a>b?a:b;} inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f; } inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ; } int n,m; int c[500006]; int lowbit(int x){return x&(-x);} void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;} int solve(int i){ int sum=0; for(;i>=1;i-=lowbit(i)) sum+=c[i]; return sum; } int main(){ n=rd(),m=rd(); for(int i=1;i<=n;i++){ int x=rd(); build(i,x); } for(int i=1;i<=m;i++){ int f=rd(); int x=rd(),y=rd(); if(f==1) build(x,y); else write(solve(y)-solve(x-1)),puts(""); } return 0; }
然後就是一個小小的修改了
如何用樹狀陣列來維護區間修改+單點查詢
大家可以先自己想一想(反正我當時是沒有想出來的)
不太會的同學不要擔心
因為這裡的樹狀陣列和我們剛才講的不太一樣
哪裡不一樣呢,就是這裡的C陣列不是用來存和的
而是被用來存一個叫做差分的東西
什麼是差分呢,就是對於一個數組
我們不維護每個地方的值,而是維護一個字首和
使得從下標1加到下標x,就剛好可以得到原組的第x個元素
雖然查詢變慢了,但是區間修改只需要O(1)的時間
為什麼如此神奇呢,我們來舉個例子
現在我們需要將2到4的區間加上1
我們就把差分陣列下標為2的地方加1,下標為4+1的地方減1
就變成了:
計算字首和,得到序列0 1 1 1 0 0 和原陣列保持一致
是不是很神奇呢
而區間修改則是用樹狀陣列來維護差分
雖然把修改變成了logn
但是相應的,單點查詢也變成了logn
看似血虧,實則血賺
經過了上文的講解,這裡的具體操作我就不贅述了
再來一道題:樹狀陣列模板2
附上AC程式碼:
#include<iostream> #include<cmath> #include<cstdio> #include<cstdlib> #include<cstring> #include<string> #include<algorithm> using namespace std; inline int min(int a,int b){return a<b?a:b;} inline int max(int a,int b){return a>b?a:b;} inline int rd(){ int x=0,f=1; char ch=getchar(); for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1; for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0'; return x*f; } inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return ; } int n,m; int c[500006]; int lowbit(int x){return x&(-x);} void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;} int solve(int i){ int sum=0; for(;i>=1;i-=lowbit(i)) sum+=c[i]; return sum; } int main(){ n=rd(),m=rd(); int set=0; for(int i=1;i<=n;i++){ int x=rd(); build(i,x-set); set=x; } for(int i=1;i<=m;i++){ int f=rd(); if(f==1){ int x=rd(),y=rd(),k=rd(); build(x,k);build(y+1,-k); } else{ int x=rd(); write(solve(x)),puts(""); } } return 0; }
總而言之,樹狀陣列還是很好的一種資料結構
只要利用得當,每一種資料結構都能夠煥發出耀眼的光芒,給程式碼帶來無限生機