樹狀數組
一、用處
有時候題目會要求維護一個數組的前綴和,朴素調整的話最壞是O(n)的復雜度
而當我們學會了 “樹狀數組” ,他的修改與求和都是O(logn)的
常見用於:
(1)單點修改,區間查詢
(2)區間修改,單點查詢(差分實現)
二、基本思想
任意一個正整數都可以被 “二進制分解”
比如區間 [1,n] 可以分解成 logx個小區間
樹狀數組就是就是基於以上操作的一種數據結構,基本用途是維護前綴和。對於區間[1, x ] ,樹狀數組將他分解為logx個子區間,從而滿足快速詢問區間和。
三、基本算法
子區間的共同特點是:若區間結尾為R,則區間長度就等於R的“二進制分解”下的最小二次冪,設為lowbit(R)
對於給定的序列A,建立一個數組c,c[x]保存序列A的區間 [ x-lowbit(x)+1,x ] 中所有數字的和
你看下面這個圖:
該結構滿足以下性質:
(1)每個內部節點c[x]保存以他為根的子樹中所有葉節點的和
(2)每個內部節點c[x]的子節點數等於lowbit(x)的大小
(3)除數根外,每個內部節點c[x]的父節點是c[x+lowbit(x)]
(4)樹的深度為O(logN)
1.求lowbit(x)

int lowbit(int x) { return x&-x; }
2.單點修改
當我們修改了單點的值,與它相關的父節點的值也會相應的發生改變,上傳維護,由子及父

void updata(int x,int v) { while(x<=n) { c[x]+=v; x+=lowbit(x); } }
3.查詢前綴和
由父及子

int sum(int x) { int ans=0; while(x>0) { ans+=c[x]; x-=lowbit(x); } return ans; }
4.區間求和
Σx~y = sum(y) - sum(x-1)
5.擴展(多維樹狀數組)
如果有n*m的二維數組a,樹狀數組為c,那么單點修改和求前綴和就有以下操作:

int updata(int x,int y,int z) { int i=x; while(i<=n) { int j=y; while(j<=m) { c[i][j]+=z; j+=lowbit(j); } i+=lowbit(i); } }

int sum(int x,int y) { int ans=0,i=x; while(i>0) { int j=y; while(j>0) { ans+=c[i][j]; j-=lowbit(j); } i-=lowbit(i); } return ans; }
6.注意事項
樹狀數組能處理的是下標為1~n的數組,下標絕對不能為0,lowbit(0)=0,這樣會陷入死循環
四、典型例題
(1)單點修改,區間查詢
P3374 【模板】樹狀數組 1
非常正宗的板子題了

#include<bits/stdc++.h> using namespace std; const int maxn=5e5+10; int n,m,opr,x,y,k; int c[maxn]; inline int read() { int ans=0; char last=' ',ch=getchar(); while(ch<'0'||ch>'9') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } int lowbit(int x) { return x&-x; } void updata(int x,int v) { while(x<=n) { c[x]+=v; x+=lowbit(x); } } int sum(int x) { int ans=0; while(x>0) { ans+=c[x]; x-=lowbit(x); } return ans; } int main() { n=read();m=read(); for(int i=1;i<=n;i++) { k=read(); updata(i,k); } for(int i=1;i<=m;i++) { opr=read();x=read();y=read(); if(opr==1) updata(x,y); if(opr==2) printf("%d\n",sum(y)-sum(x-1)); } return 0; }
(2)區間修改,單點查詢
P3368 【模板】樹狀數組 2
題解
這里是用差分來實現
什么是差分??
給出一個數列 A1 A2 A3 A4 A5 。。。。An
用數組 c[ i ] 來記錄A i 與 A i-1的差,即 c[ i ] = A[ i ] - A[ i-1 ]
那么當我們想要修改區間 [ x,y ]的值的時候,區間里每個數都加上相同的數字,c[i+1]~c[j]都是不變的,改變的只是 c[ i ] 和 c[ j+1 ] ,由於是區間加,c[ i ] 自然就變大了,c[ j+1 ] 自然就變小了
這時用二維數組維護差分數組就行了,每次區間修改只需要改兩個值
單點查詢呢? A x = Σ c[ i ] (i=1~i)

#include<bits/stdc++.h> using namespace std; const int maxn=5e5+10; int n,m,opr,x,y,k; int c[maxn],a[maxn]; inline int read() { int ans=0; char last=' ',ch=getchar(); while(ch<'0'||ch>'9') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } int lowbit(int x) { return x&-x; } void updata(int x,int v) { while(x<=n) { c[x]+=v; x+=lowbit(x); } } int sum(int x) { int ans=0; while(x>0) { ans+=c[x]; x-=lowbit(x); } return ans; } int main() { n=read();m=read(); for(int i=1;i<=n;i++) { a[i]=read(); updata(i,a[i]-a[i-1]); } for(int i=1;i<=m;i++) { opr=read(); if(opr==1) { x=read();y=read();k=read(); updata(x,k); updata(y+1,-k); } if(opr==2) { x=read(); printf("%d\n",sum(x)); } } return 0; }
五、后記
能用樹狀數組做的題,線段樹也能做;
但能用線段樹做的,樹狀數組不一定能做。
它比線段樹優秀是什么情況呢??
- 線段樹常數過大時
- 線段樹功能過多時
樹狀數組可求的所有問題必須存在逆元