1. 程式人生 > >樹狀陣列3種基本操作

樹狀陣列3種基本操作

# 告知 本部落格是由一個蒟蒻編寫,內容可能出錯,若發現請告訴本蒟蒻,以便大眾閱讀 **轉載請註明原網址:https://www.cnblogs.com/H-K-H/p/14083914.html** # 樹狀陣列和線段樹 ~~眾所周知,~~ 線段樹和樹狀陣列是兄弟來的 ## 它們之間的關係 *樹狀陣列可以解的,線段樹能解 樹狀陣列不可以解的,線段樹還是可以解* 既然這樣,那我學會線段樹不就搞定了嗎,幹嘛還學樹狀陣列呀 ## 那麼,樹狀陣列優在何處呢? 其實呢,就是==碼量少,思維清晰==吧 對比一下 單點修改區間查詢 線段樹100行起步 樹狀陣列呢,50行左右吧 區間修改區間查詢 線段樹估計要飆到150了吧 樹狀陣列依舊50行 沒有對比就沒有傷害呀 這時,有些線段樹忠實粉或許會思考人生:**你看我還有機會嗎?** 機會是有的,那就是,打樹狀陣列吧(當然有些題還是要打線段樹的啦) # 樹狀陣列簡介 ## 樹狀陣列圖解 此章節內容部分引用自[**bestsort的小站**](https://bestsort.cn/2019/04/26/195/) 眾所周知,一棵滿二叉樹長樣: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200726201132496.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xaWF9seng=,size_16,color_FFFFFF,t_70#pic_center) 挪一下位置後,變成了這樣 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200726201326208.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xaWF9seng=,size_16,color_FFFFFF,t_70#pic_center) 上面這個就是樹狀陣列的畫法 準確來說,這時求和陣列的畫法 把原陣列$a$也加進來,成了這樣($c$是求和陣列)![在這裡插入圖片描述](https://img-blog.csdnimg.cn/202007262017285.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xaWF9seng=,size_16,color_FFFFFF,t_70#pic_center) $c[i]$表示子樹葉子節點的權值 如上圖,有 $c[1]=a[1]\\ c[2]=a[1]+a[2]\\ c[3]=a[3]\\ c[4]=a[1]+a[2]+a[3]+a[4]\\ c[5]=a[5]\\ c[6]=a[5]+a[6]\\ c[7]=a[7]\\ c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]$ 轉換成二進位制再來看一眼 $c[1]=c[0001]=a[1]\\ c[2]=c[0010]=a[1]+a[2]\\ c[3]=c[0011]=a[3]\\ c[4]=c[0100]=a[1]+a[2]+a[3]+a[4]\\ c[5]=c[0101]=a[5]\\ c[6]=c[0110]=a[5]+a[6]\\ c[7]=c[0111]=a[7]\\ c[8]=c[1000]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]$ 對照式子可以發現,對於一個$i$ $c[i]=a[i-2^k+1]+a[i-2^k+2]+a[i-2^k+3]……+a[i]$($k$為二進位制下$i$最低位的1後面的0的個數,例如8對應的$k$就等於3,因為$8_{10}=1000_2$,最低位的1後面有3個0) 這時候,問題就來了,$2^k$怎麼求??? ## 引入$lowbit$ $lowbit$函式就是用來求$2^k$是多少的 具體操作是 ```cpp int lowbit(int x) {return x&(-x);} ``` 解釋 “&”這個符號在C++中指的是**按位與**運算,具體是說,若在二進位制下相同的位置兩數都為1,那麼&出的答案這一位也為1,否則為0 例如$12\&6$ $12_{10}=1100_2$ $6_{10}=0110_2$(空位用0補齊) $ans=0100_2=4_{10}$ 在上面這個資料中,12和6只有第三個位置上才**都是**1,那麼答案也就只有這個位置上是1 ~~( 不過學樹狀陣列的人應該都不會不知道位運算吧)~~ 那麼$x\&(-x)$是什麼意思呢 首先說明$-x$在二進位制下和$x$的關係 在二進位制下,$-x$就是$x$取反後再加1 例如,$10_{10}=01010_2$,那麼$-10_{10}=10101_2+1_2=10110_2$(第一位是符號位) 進行按位與運算後,答案就是$00010_2=2^1=2_{10}$(第一位是符號位) 眼睛掃一掃,發現答案就是$2$ 神奇吧 具體證明呢,我也不會,嘻嘻(畢竟我只是一個蒟蒻) # 基本應用 ## 1.單點修改,區間查詢 ### 修改 若要更新當前節點的$a[i]$ 那麼是不是可以直接更新$a[i]$的上級,$a[i]$上級的上級,以此類推 用$lowbit$到上級所在下標 ```cpp void update(int now,int x) { int i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; } ``` ### 查詢 對於區間查詢,我們採取字首和的求法 對於一個區間$[l,r]$,我們求出$r$的字首和,減去$l-1$的字首和即為答案 查詢的具體過程呢,也很簡單 就是從要查的節點以此往下,搜尋下級 依舊是用$lowbit$ ```cpp int get(int x) { int i,ans; ans=0; for (i=x;i>=1;i-=lowbit(i)) ans+=c[i]; return ans; } ``` ### 題目 [Loj#130 樹狀陣列 1 :單點修改,區間查詢](https://loj.ac/problem/130) ### Code ```cpp #include #include using namespace std; long long n,m,i,x,y,ch,c[1000005]; long long lowbit(long long x) { return x&(-x); } void update(long long now,long long x) { long long i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; } long long get(long long x) { long long i,ans; ans=0; for (i=x;i>=1;i-=lowbit(i)) ans+=c[i]; return ans; } int main() { scanf("%lld%lld",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&x); update(i,x); } for (i=1;i<=m;i++) { scanf("%lld%lld%lld",&ch,&x,&y); if (ch==2) printf("%lld\n",get(y)-get(x-1)); else update(x,y); } return 0; } ``` ## 2.區間修改,單點查詢 ### 修改 引入差分的思想,記錄數組裡每個元素與前一個元素的差,那麼$a_i=\sum_{j=1}^i d_j$,如果修改區間$[l,r]$,令其加上$x$,那麼$l$與$l-1$的差增加了$x$,$r$與$r+1$的差減小了$x$,根據差分,就可以給$d_{l}$加上$x$,給$d_{r+1}$減去$x$ ### 查詢 直接根據$a_i=\sum_{j=1}^i d_j$,查字首和就好 ### 題目 [Loj#131 樹狀陣列2:區間修改,單點查詢](https://loj.ac/p/131) ### Code ```cpp #include using namespace std; int n,m,i,l,r,x,bj; long long a[1000005],c[1000005]; int lowbit(int x) { return x&(-x); } void update(int now,int x) { int i; for (i=now;i<=n;i+=lowbit(i)) c[i]+=x; } long long get(int x) { int i; long long ans; ans=0; for (i=x;i;i-=lowbit(i)) ans+=c[i]; return ans; } int main() { scanf("%d%d",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&a[i]); update(i,a[i]-a[i-1]); } for (i=1;i<=m;i++) { scanf("%d",&bj); if (bj==1) { scanf("%d%d%d",&l,&r,&x); update(l,x); update(r+1,-x); } else { scanf("%d",&x); printf("%lld\n",get(x)); } } return 0; } ``` ## 3.區間修改,區間查詢 這個也是線段樹最麻煩的地方,通常100行起步,但樹狀陣列就不用了,實測50行不到,而且我**不壓行** 先看一下如果按照問題2的方法來求區間字首和,要怎麼求 位置$x$的字首和=$\sum_{i=1}^x\sum_{j=1}^id_j$,發現在這個式子裡,$d_1$被計算了$x$此,$d_2$被計算了$x-1$次……,$d_x$被計算了1次。那麼這個式子就可以轉化為 $\sum_{i=1}^xd_i\times(x-i+1)=(x+1)\sum_{i=1}^xd_i-\sum_{i=1}^xd_i\times i$ 其中$x+1$是給出的,那麼我們記錄$d_i$和$d_i\times i$就可以了 維護兩個陣列$sum1$和$sum2$,分別記錄$d_i$和$d_i\times i$ ### 修改 $sum1$同問題2的$d$,$sum2$也類似,$l$加上$l\times x$,$r+1$減去$(r+1)x$ ### 查詢 單點$x$的字首和就是$(x+1)\times sum1$中$x$的字首和-$sum2$中$x$的字首和,區間$[l,r]$的值就是$r$的字首和-$l-1$的字首和 ### 題目 [Loj#132 樹狀陣列3:區間修改,區間查詢](https://loj.ac/p/132) ### Code ```cpp #include using namespace std; long long n,m,i,l,r,x,bj,a[1000005],c1[1000005],c2[1000005]; long long lowbit(long long x) { return x&(-x); } void update(long long k,long long x) { long long i; for (i=k;i<=n;i+=lowbit(i)) { c1[i]+=x; c2[i]+=x*k; } } long long get(long long x) { long long i,ans; ans=0; for (i=x;i;i-=lowbit(i)) ans+=((x+1)*c1[i])-c2[i]; return ans; } int main() { scanf("%lld%lld",&n,&m); for (i=1;i<=n;i++) { scanf("%lld",&a[i]); update(i,a[i]-a[i-1]); } for (i=1;i<=m;i++) { scanf("%lld",&bj); if (bj==1) { scanf("%lld%lld%lld",&l,&r,&x); update(l,x); update(r+1,-x); } else { scanf("%lld%lld",&l,&r); printf("%lld\n",get(r)-get(l-1)); } } return 0; } ``` --------- # 小結 線段樹與樹狀陣列有很多相似的地方,但是樹狀陣列很明顯的優勢就是短,但是線段樹可以處理很多種情況,而這裡面有些是樹狀陣列做不到的,所以說不論是線段樹還是樹狀陣列,我們都應該學習一下,然後選擇更好的去解決題目。 不定時更新高