下面是樹狀數組的學習筆記:ios
在解題過程當中,有時候要維護一個數組的前綴\(S[i]=A[1]+A[2]+···+A[i]\)。可是若是咱們修改了任何一個\(A[i]\),\(S[1]、S[2]、S[3]···S[i]\)的值都會相應改變。若是咱們循環維護每個\(S[i]\),那樣就太慢了。因而咱們要考慮使用數據結構來進行優化。樹狀數組就是這樣一個數據結構!數組
首先介紹一下前置知識:數據結構
根據任意正整數關於\(2\)的不重複次冪的惟一分解性質,若一個正整數\(x\)的\(2\)進製表示爲\(10101\),其中等於\(1\)的位是\(0,2,4\)則正整數能夠被分解爲\(2^4+2^2+2^0\)。那麼,區間\([1,x]\)就能夠被分爲\(logx\)各小區間函數
長度爲\(2^4\)的區間爲\([1,2^4]\);長度爲\(2^2\)的區間爲\([2^4+1,2^4+2^2]\);長度爲\(2^0\)的區間爲\([2^4+2^2+1,2^4+2^2+2^0]\)。學習
樹狀數組就是這樣一種結構,它的基本用途就是維護序列的前綴和。對於區間\([1,x]\)樹狀數組將其分爲\(logx\)個子區間,從而知足快速詢問區間和。優化
由上文可知,這些子區間的共同特色是:若區間結尾爲\(R\)則區間長度就爲\(R\)的「二進制分解」下最小的\(2\)的次冪,咱們定義爲\(lowbit(R)\)。對於給定的\(A\)序列,咱們維護一個\(c\)數組,其實這個\(c\)數組就是咱們所說的樹狀數組。\(c[i]\)中儲存的是\(\sum A[i]\space (i\in [x-lowbit(x)+1,x])\)。spa
樹狀數組的存儲結構以下圖:code
值得注意的是,這樣的樹狀結構知足如下的4個性質:get
\(1\). 每一個內部節點\(c[i]\)保存以它爲根的子樹中全部葉節點的和it
\(2\). 每一個內部節點\(c[i]\)的子節點個數爲\(lowbit(i)\)的大小
\(3\). 除樹根外,每一個內部節點\(c[i]\)的父節點是\(c[i+lowbit(i)]\)
\(4\). 樹的深度爲\(O(logN)\)
接下來就講到實現方面了,樹狀數組的實現是與上面\(4\)個性質分不開的。
\(lowbit(n)\)表示去除非負整數\(n\)在二進制下最低位的\(1\)以及它後邊的\(0\)構成的數值。因而\(lowbit(n)=n\)&\((-n)\),可是這是怎麼來的呢,請聽我細細道來(其實我也不知道)
設\(n>0\),\(n\)的第\(k\)位是1,其他都是\(0\)
爲了實現\(lowbit\)運算,先把\(n\)取反,此時第\(k\) 位變爲\(0\),其他都是\(1\)。再另\(n=n+1\),此時由於進位 ,第\(k\)位變爲\(1\),其他都爲\(0\)。在上面的取反加一操做後,\(n\)的第\(k+1\)到最高位剛好與原來相反,因此\(n\)&~\(n+1\)僅有第\(k\)位爲\(1\),其他位都是\(0\)。而在補碼錶示下,~\(n=-1-n\),所以,\(lowbit(n)=n\)&\((-n)\)
下面是這一部分的代碼實現:
int lowbit(int n) { return n&(-n);//這句話水的不能再水了,不說了 }
樹狀數組支持單點操做,根據性質\(一、3\)。每個內部節點\(c[i]\)保存以它爲根的子樹中全部葉節點的和,這就說明,咱們一旦對$A[i] \(進行改動,就必定要對\)c[i]\(進行改動,而且將全部\)c[i]\(的父親節點改動。那麼如何找到\)c[i]\(的父親節點呢?其實咱們只須要將\)i\(更新爲\)i+lowbit(i)$,就能夠完成對父親節點的遍歷了。
下面是這一部分的代碼實現:
void update(int x,int y)//表示A[x]加y時維護c數組 { for(;x<=n;x+=lowbit(x)) c[x]+=y; }
樹狀數組支持單點修改和區間查詢,下面介紹一下區間查詢前綴和的方式:
在這裏咱們所定義的前綴和,實際上就是\(\sum A[i] (i\in [1,x])\)。按照(\(1\))中所講,應該將$[1,x] \(分紅\)logn\(個小區間,每一個小區間的區間和都已經保存在數組\)c$中。
下面是這一部分的代碼實現:(即\(O(logn)\)時間查詢前綴和)
int sum(int x)//求1~x的前綴和 { int ans=0;//用ans存儲前綴和 for(;x;x-=lowbit(x)) ans+=c[i];//遍歷子節點,累加ans return ans;//最後的答案就是ans }
調用以上的\(sum\)函數,能夠求出\(A[x]···A[y]\)得值就是\(sum(y)-sum(x-1)\)
因爲處理每個樹狀數組的複雜度是\(O(logn)\)。因此它能夠擴充到\(m\)維,這樣一來,處理樹狀數組的複雜度就提高到了\(O(log^mn)\),若\(m\)不大的時候,這個複雜度是能夠被接受的。可是到底如何實現呢?
其實,擴充樹狀數組的方式就是講原來的一個循環改爲\(m\)個循環,這一部分的代碼以下:
void update(int x,int y,int z)//將(x,y)的值加上z { int i=x; while(i<=n)//若是m更大,再多寫幾個while { int j=y; while(j<=m) { c[i][j]+=z; j+=lowbit(j); } i+=lowbit(i); } }
int sum(int x,int y) { int res=0,i=x; while(i>0) { int j=y; while(j>0) { res+=c[i][j]; j-=lowbit(i); } i-=lowbit(i); } return res; }
要注意樹狀數組絕對不能出現下標爲\(0\)的情況,由於啥啊,由於\(lowbit(0)=0\)啊!
樹狀數組的應用有不少,下面重點將洛谷上兩個模板題講解一下
【題目描述】
如題,已知一個數列,你須要進行下面兩種操做:
\(1\).將某一個數加上\(x\)
\(2\).求出某區間每個數的和
直接上代碼吧,就是不折不扣的板子題
#include<cstdio> #include<iostream> using namespace std; const int maxn=500010; int n,m; int a[maxn],c[maxn]; int h,q,k;//我沒得設變量啊TwT int lowbit(int x)//就是算lowbit(x) { return x&(-x); } void update(int x,int y)//就是將y插入到x中 { for(;x<=n;x+=lowbit(x)) { c[x]+=y; } } int sum(int x)//就是計算A[1]+A[2]+···+A[x]的值 { int ans=0; for(;x;x-=lowbit(x)) { ans+=c[x]; } return ans; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) { scanf("%d",&a[i]); update(i,a[i]); } while(m)//循環讀入數據 { m--; scanf("%d",&h); if(h==1)//h是對類型的判斷 { scanf("%d%d",&q,&k); update(q,k); } if(h==2) { scanf("%d%d",&q,&k); printf("%d\n",sum(k)-sum(q-1)); } } return 0; }
上面的這道題是典型的樹狀數組「單點修改,區間查詢題」。可是下面一道題就不是這樣了!
【題目描述】
如題,已知一個數列,你須要進行下面兩種操做:
\(1\).將某區間每個數數加上\(x\)
\(2\).求出某一個數的值
【題目分析】
誒?這道題好像是「區間修改,單點查詢」的題額,怎麼作呢?咱們引入一個叫作差分的概念:
假設\(A[]=\){\(1,5,3,6,4\)},\(B[]=\){\(1,4,-2,3,-2\)}
咱們能夠發現,若是規定\(A[0]=0\),那麼能夠知足\(B[i]=A[i]-A[i-1]\),而且\(A[i]=B[1]+B[2]+···+B[i]\)。
若是咱們在區間\([2,4]\)上加上\(2\),按照相同的規則進行處理,能夠獲得\(B[]=\){\(1,7,-2,3,\)},只有\(B[2]\)和\(B[5]\)有改動
因而咱們能夠獲得一個規律,即:對於\(A\),將區間\([l,r]\)中的每個數都加上一個數\(x\),\(B[l]+=x;B[r+1]-=x\)
因而咱們能夠開一個樹狀數組維護差分值,每次只須要將\(l\)和\(r+1\)進行操做,最後將須要查詢的\(A\)值轉化爲\(B\)值求和就能夠獲得了!
下面請食用代碼:
#include<cstdio> #include<iostream> using namespace std; const int maxn=500010; int n,m; int a[maxn],c[maxn]; int h,q,k,d; int lowbit(int x) { return x&(-x); } void update(int x,int y) { for(;x<=n;x+=lowbit(x)) { c[x]+=y; } } int sum(int x) { int ans=0; for(;x;x-=lowbit(x)) { ans+=c[x]; } return ans; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d",&a[i]); while(m) { m--; scanf("%d",&h); if(h==1) { scanf("%d%d%d",&q,&k,&d); update(q,d); update(k+1,-d); } if(h==2) { scanf("%d",&q); printf("%d\n",a[q]+sum(q)); } } return 0; } /* 相信你們已經發現了,這段代碼與前一段沒有什麼太大的區別,其實就是將讀入增長了一個,輸出減小了一個罷了 */
\(end\)