樹狀數組總結(不看後悔系列)

最近準備學深點樹狀數組,在原來的文章上添寫內容。

定義

樹狀數組或二元索引樹(英語:Binary Indexed Tree/(BIT)),又以其發明者命名爲Fenwick樹,其初衷是解決數據壓縮裏的累積頻率(Cumulative Frequency)的計算問題,現多用於高效計算數列的前綴和, 區間和。它可以O(logn){\displaystyle O(\log n)}的時間得到任意前綴和i=1jA[i],1<=j<=N{\displaystyle \sum _{i=1}^{j}A[i],1<=j<=N}{\displaystyle },並同時支持在O(logn){\displaystyle O(\log n)}時間內支持動態單點值的修改。空間複雜度O(n){\displaystyle O(n)}

結構起源

按照Peter M. Fenwick的說法,正如所有的整數都可以表示成2的冪和,我們也可以把一串序列表示成一系列子序列的和。採用這個想法,我們可將一個前綴和劃分成多個子序列的和,而劃分的方法與數的2的冪和具有極其相似的方式。一方面,子序列的個數是其二進制表示中1的個數,另一方面,子序列代表的f[i]的個數也是2的冪。

預備函數——lowbitlowbit函數

定義一個lowbitlowbit函數,返回參數轉爲二進制後,最後一個1的位置所代表的數值.
求法:

int lowbit(int x){ return x & (-x);}

列如6:01106: 0110,返回的是2

關於樹狀數組的理解

藉助 oi wiki 一張圖來描述。
在這裏插入圖片描述

定義樹狀數組CC

CC中節點xx維護原數組AA的區間和[xlowbit(x)+1,x][x-lowbit(x)+1,x]

C[x]=i=xlowbit(x)+1xA[i]C[x]=\sum\limits_{i=x-lowbit(x)+1}^{x}A[i]

所以在構造樹狀數組的時候可以前綴和O(n)O(n)構造。

然後舉個例子,對於x=7=22+21+20x=7=2^2+2^1+2^0,區間[1,7][1,7]可以分解爲[1,4],[5,6],[7,7][1,4],[5,6],[7,7]三個小區間,每個區間的長度對應着二進制分解後的大小,且每個小區間的長度也等於區間右段點的lowbitlowbit的值。

該結構滿足如下性質:
1、每個內部節點c[x]c[x]保存以它爲根的子樹中所有葉節點的和。

2、每個內部節點c[x]c[x]的子節點個數等於lowit(x)lowit(x)位數

3、除樹根外,每個內部節點c[x]c[x]的父節點是c[x+lowbit(x)]c[x+lowbit(x)]

4、樹的深度爲O(log(N))O(log(N))

然後下面是引用知乎上得一句話。

我們知道樹狀數組是可以用來求前綴和的。而線段樹樹求前綴和時是不需要右兒子的,所以把右兒子全部去掉,只保留左區間就是BIT了。

而維護的區間則是[i-lowbit(x)+1,i];其中lowbit爲結點i二進制最低位1的值。而lowbit操作就是這課二叉樹的層,而BIT的實質上也是在Binary上建個tree。所以modify的時候就沿着邊往上爬;求前綴和時就往下爬。

但還有個問題就是,區間修改,然後再單點查詢怎麼辦?這時就在tree上構建差分數組,然後每次change的時候<就只修改[l,+k],[r+1,-k]即可。之後再把要查詢的加起來就是了。————————————————Violetinori

二維樹狀數組

二維樹狀數組常用來維護子矩陣的和。
和一維類似,二維樹狀數組每一維都是樹狀數組。
例:舉個例子來看看C[][]的組成。
設原始二維數組爲:
  A[][]A[][]={{a11,a12,a13,a14,a15,a16,a17,a18,a19a11,a12,a13,a14,a15,a16,a17,a18,a19},
   \ \ \ \qquad {a21,a22,a23,a24,a25,a26,a27,a28,a29a21,a22,a23,a24,a25,a26,a27,a28,a29},
   \ \ \ \qquad {a31,a32,a33,a34,a35,a36,a37,a38,a39a31,a32,a33,a34,a35,a36,a37,a38,a39},
   \ \ \ \qquad {a41,a42,a43,a44,a45,a46,a47,a48,a49a41,a42,a43,a44,a45,a46,a47,a48,a49}};
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,…
這是A[][]第一行的一維樹狀數組

C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,…
這是A[][]數組第一行與第二行相加後的樹狀數組

C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,…
這是A[][]第三行的一維樹狀數組

C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,…
這是A[][]數組第一行+第二行+第三行+第四行後的樹狀數組

ll c[N+5][N+5];
int n,m;
inline int lowbit(int x) {return x&(-x);}
void add(int i,int j,int val){
    for(int x = i;x <= n;x += lowbit(x))
        for(int y = j;y <= m;y += lowbit(y))
            c[x][y] += val;
}
ll ask(int i,int j){
    ll ans = 0;
    for(int x = i;x>0;x -= lowbit(x))
        for(int y = j;y > 0;y -= lowbit(y))
            ans += c[x][y];
    return ans;
}
ll sum(int x,int y,int xx,int yy){
    x --;y --;
    return ask(xx,yy) - ask(xx,y) - ask(x,yy) + ask(x,y);
}

功能

說到底,樹狀數組最主要的作用就是動態維護前綴和。
注意:樹狀數組的下標必須從1開始,,因爲lowbit(0)=0,如果從0開始的話就會陷入死循環!!樹狀數組適用於所有滿足結合律的運算(加法,乘法,異或等)

1、查詢區間前綴和

從上面我們可以知道,[1,7][1,7]在樹狀數組中被分解爲3個小區間,每個小區間爲[xlowbit(x)+1,x][x-lowbit(x)+1,x]。而xx是樹狀數組中的節點。所以要查詢[1,7][1,7]的前綴和,只需要從頂點77不斷往下跳lowbitlowbit累加即可。

int ask(int x){
    int ans = 0;
    while(x){
        ans += c[x];
        x -= lowbit(x);
    }
    return ans;
}

所以我們要求[l,r][l,r]的區間和,區間和滿足區間可減性,即ask(r)ask(l1)ask(r)-ask(l-1),

2、單點修改,維護前綴和

注意在樹狀數組中包含原數組中a[x]a[x]的只有c[x]c[x]及其它的祖先節點。並且任意節點的祖先至多有log(N)log(N)個。所以我們在xx點邊修改邊網上跳lowbitlowbit即可。

void add(int x,int y){//n爲區間上界
    for(;x <= n;x += lowbit(x)) c[x] += y;
}

樹狀數組的構造

比較一般的是直接建立一個全爲00的數組cc,然後對每個位置xx執行add(x,a[x])add(x,a[x]),時間複雜度O(nlog(N))O(nlog(N))

for(int i = 1;i <= n;++i) add(i,a[i]);

關於O(N)O(N)的構造上面已經提到了,因爲每個節點xx以它爲右端點,長度爲lowit(x)lowit(x)的一段區間,所以我們邊維護原數組aa的前綴和,邊構造cc

void init(){
    for(int i = 1;i <= n;++ i){
        sum[i] = sum[i-1] + a[i];
        c[i] = sum[i] - sum[i-lowbit(i)];
    }
}

還有個O(N)O(N)構造
每一個節點的值是由所有與自己直接相連的兒子的值求和得到的。因此可以倒着考慮貢獻,即每次確定完兒子的值後,用自己的值更新自己的直接父親。


void init(){
    for(int i = 1;i <= n;++i){
        c[i] += a[i];
        int j = i + lowbit(i);
        if(j <= n) c[j] += c[i];
    }
}

樹狀數組黑科技

  • 樹狀數組維護區間最值

我們有上述可以發現,樹狀數組又似一種分塊方式,將[1,r][1,r]分爲若干子區間,然後每個節點維護這一段區間區間和,而如果我們讓每個節點維護區間最值(以下假如是最大值),那麼類比於查詢區間和功能,我們改下得:

int ask(int x){
    int ans = 0;
    while(x){
        ans = max(ans,c[x]);
        x -= lowbit(x);
    }
    return ans;
}

但是卻有一個問題,那就是這樣只能查詢[1,x][1,x]的最值,而不能查詢[l,r][l,r]的最值,因爲區間最值不滿足區間可加/減性。但也不妨礙我們使用它,比如在求LISLIS的過程中,原來做法n2n^2,因爲每次找前面比他小的最大的LISLIS都要O(n)O(n)遍歷一遍,找到這個最大值,注意此處是找[1,i][1,i]的最大值,所以我們完全可以使用樹狀數組來優化這個過程變爲O(log(n))O(log(n))。這樣整體複雜度就降爲了O(nlog(N))O(nlog(N)),關於LIS這部分講解,以後應該會放出來吧。

至於網上其他的改法什麼的個人感覺複雜度不大靠譜,還是老老實實學線段樹去吧。

  • lognlogn求第k小
    如果我們把每個值當做下標插入進去(add(ai,1))(add(a_i,1)),我們很容易求的某個數前面有多少個數小於等於它(ask(ai))(ask(a_i))在樹狀數組中,節點是根據 2 的冪劃分的,每次可以擴大 2 的冪的長度()(倍增思想)。當然這也離不開離散化,我會在最後一道題講解下這個用法。下面這張圖比較直觀的說明了樹狀數組的結構性質。
    (圖片來自網絡)
    img

  • 區間操作

1、區間修改,單點查詢————樹狀數組維護差分數組

樹狀數組只能查詢前綴和 和 單點修改,我們可以通過設立一個差分數組b[ ]b[\ ]

並用樹狀數組維護,那麼區間修改就變爲了單點修改。如在[l,r][l,r]的區間內增加一

kk,那麼add(l,k),add(r+1,k)add(l,k),add(r+1,-k)。這就完成了差分數組的維護,若單點查

詢x處 的值,只需a[x]+ask(x)a[x] + ask(x)

2、區間修改,區間查詢————兩個樹狀數組維護(可以用來完成動態區間 加 等差數列的維護)

由上面我們得知,用樹狀數組進行區間修改就是維護一個差分數組bb,對於xx位置

處增加的值就是ask(x)ask(x),即i=1xb[i]\sum_{i=1}^{x}b[i],那麼對於原數組a[ ]a[\ ],數組aa的前綴和

S(x)S(x)總體增加的值就是i=1xj=1ib[j]\sum_{i=1}^{x}\sum_{j=1}^{i}b[j]

i=1xj=1ib[j]=\sum_{i=1}^{x}\sum_{j=1}^{i}b[j]=

i=1x(xi+1)b[i]=(x+1)i=1xb[i]i=1xib[i]\qquad\qquad\sum_{i=1}^{x}(x-i+1)*b[i]=(x+1)\cdot \sum_{i=1}^{x}b[i]-\sum_{i=1}^{x}i\cdot b[i]

即我們可以用兩個樹狀數組維護上述兩個前綴和。。(其中前一個是差分數組,後面是i*差分數組)

具體的說,我們建立兩個樹狀數組c0,c1,起初全部賦值爲0.對於每條指令 C l r d 執行四個操作

1、在樹狀數組c0中把位置 l 上的加上d

2、在樹狀數組c0中,把位置r+1 上的位置減去d

3、在樹狀數組c1中,在位置l 上加上l * d

4、在樹狀數組c1中,在位置r+1上的數減去(r+1)*d

另外,我們建立數組S存儲a的原始前綴和。所以對於每條指令 Q l r爲:

[S(r)+(r+1)ask(c0,r)ask(c1,r)][S(l1)+lask(c0,l1)ask(c1,l1)][S(r)+(r+1)*ask(c0,r)-ask(c1,r)]-[S(l-1)+l*ask(c0,l-1)-ask(c1,l-1)]

雖然這樣區間修改區間維護這個做法比較麻煩,不如直接用線段樹,不過,有時候動態區間加等差數列會產生比較巧妙的用處。

附上算法競賽進階指南上的代碼

const int SIZE = 100010;
int a[SIZE],n,m;
ll c[2][SIZE],S[SIZE];
int lowbit(int x){return  x&(-x);}
ll ask(int k,int x){
    ll ans = 0;
    for(;x > 0;x -= lowbit(x)) ans += c[k][x];
    return ans;
}
void add(int k,int x,int y){
    for(;x <= n;x += lowbit(x)) c[k][x] += y;
}
int main(){
    cin >> n >> m;
    for(int i = 1;i <= n;++i) scanf("%d",&a[i]),S[i] = S[i-1] + a[i];
    while(m --){
        char op[3];int l,r,d;
        scanf("%s%d%d",op,&l,&r);
        if(op[0] == 'C'){
            scanf("%d",&d);
            add(0,l,d);add(0,r+1,-d);
            add(1,l,l*d);add(1,r+1,-(r+1)*d);
        }
        else {
            ll ans = S[r] + (r+1)*ask(0,r) - ask(1,r);
            ans -= S[l-1] + l *ask(0,l-1) - ask(1,l-1);
            printf("%lld\n",ans);
        }
    }
}

離散化

一些問題用樹狀數組解決需要離散化,比如給兩個數組ai,bia_i,b_i,這是一個二維偏序<ai,bi><a_i,b_i>,然後讓你根據題意維護前綴和。但是需要注意的是,時間是一種全序全系,4s4s就是比3s3s晚。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100000;
int a[maxn];
int b[maxn];
int n;
int main()
{
    int m=0;
    cin>>n;//元素個數
    for(int i=1;i<=n;++i)
    {
    scanf("%d",&a[i]);
    b[i]=a[i];//b[]作爲離散化的數組
    }
    sort(b+1,b+1+n);//將b數組排序,因爲是從b[1]開始存儲的,所以要b+1
    m=unique(b+1,b+1+n)-b-1;//去重操作,返回不同元素的個數
    for(int i=1;i<=n;++i)
      a[i] = lower_bound(b+1,b+1+m,a[i]) - b;//映射
    return 0;
}

例題講解

例題:給你nn個數,a1,a2.....ana_1,a_2.....a_n,然後對於每一個ii讓你求前面有多少值小於它。
1<=n<=1e5,0<=ai<=1e91<=n<=1e5,0<=a_i<=1e9
思路:
題目說每一個ii前面有多少元素小於它,位置是一個偏序,且是個全序關係。
這裏我們維護的是元素數目的前綴和。插入時,把每個元素的aia_i當做下標,值爲1。
我們正序遍歷,然後對於每個ii,我們先查詢小於aia_i的前綴和,然後在執行插入add(a[i],1)add(a[i],1)
但是aia_i的範圍很大,我們不關心aia_i的大小,我們在乎的是aia_i之間的大小關係。所以我們將aia_i離散化,就是將這些值映射爲一組排名。

poj2299
題意:讓你求有多少逆序對。
思路:相比於上個例題,這個是讓求後面有多少個元素小於他,所以我們倒序插入處理,做法類似於上面。
注意開long long。。。

ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
    ll ans = 0;
    while(x){
        ans += c[x];
        x -= lowbit(x);
    }
    return ans;
}
void add(int x,int y){
    for(;x <= n;x += lowbit(x)) c[x] += y;
}
int a[N],b[N];
int main(){
    while(scanf("%d",&n) == 1&&n){
        memset(c,0,sizeof c);
        for(int i = 1;i <= n;++i) scanf("%d",&a[i]),b[i] = a[i];
        sort(b + 1,b + n + 1);
        int m = unique(b+1,b+n+1) - b - 1;
        for(int i = 1;i <= n;++i) a[i] = lower_bound(b+1,b+m+1,a[i]) - b;
        ll ans = 0;
        for(int i = n;i >= 1;i --){
            ans += ask(a[i]-1);
            add(a[i],1);
        }
        printf("%lld\n",ans);
        //bug;
    }
}

pojpoj跑了接近800ms,接下來講講memset的時間戳優化數組清0。

什麼是時間戳?
時間戳就是記錄這是第幾次調用樹狀數組的標誌,在修改時如果發現當前的節點的時間戳不在當前時間就提前清零,如果詢問時發現當前的節點的時間戳不在當前時間顯然需要忽視這個節點,這樣的操作類似於一種lazy思想——當修改時才清除當前節點。

這樣的話在多次需要樹狀數組時就可以減少大量的不必要操作,起到十分顯著的優化效果。——————CHN_JZ

int T;//時間戳
int time[N];//儲存節點的訪問時間
ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
    ll ans = 0;
    while(x){
        ans += (time[x]<T?0:c[x]);
        x -= lowbit(x);
    }
    return ans;
}
void add(int x,int y){
    for(;x <= n;x += lowbit(x)) {c[x] = time[x] < T?y:c[x] + y;time[x] = T;}
}

完整代碼

int T;//時間戳
int time[N];//儲存節點的訪問時間
ll c[N];int n;
inline int lowbit(int x){return x & (-x);}
ll ask(int x){
    ll ans = 0;
    while(x){
        ans += (time[x]<T?0:c[x]);
        x -= lowbit(x);
    }
    return ans;
}
void add(int x,int y){
    for(;x <= n;x += lowbit(x)) {c[x] = time[x] < T?y:c[x] + y;time[x] = T;}
}
int a[N],b[N];
int main(){
    while(scanf("%d",&n) == 1&&n){
        T ++;
        for(int i = 1;i <= n;++i) scanf("%d",&a[i]),b[i] = a[i];
        sort(b + 1,b + n + 1);
        int m = unique(b+1,b+n+1) - b - 1;
        for(int i = 1;i <= n;++i) a[i] = lower_bound(b+1,b+m+1,a[i]) - b;
        ll ans = 0;
        for(int i = n;i >= 1;i --){
            ans += ask(a[i]-1);
            add(a[i],1);
        }
        printf("%lld\n",ans);
    }
}

有點尷尬的是不知道爲什麼這樣反而慢了幾十ms?應該是遠古oj太破了吧。。。

Codeforces Round #624 (Div. 3)F
題意:
nn個點在XX軸上,初始座標值分別爲a1,....ana_1,....a_n,然後每個點都有一個速度viv_i,定義d(i,j)d(i,j)是經過tt秒後,兩個點的最小距離。讓你求1<=i<j<=nnd(i,j)\sum\limits_{1<=i<j<=n}^{n}d(i,j)

思路:
我們考慮對答案的貢獻。顯然對於xi,xjx_i,x_j,不失一般性設xi<xjx_i<x_j,如果vi>vjv_i>v_j,的話兩個點遲早會相遇,也就是d(i,j)=0d(i,j)=0,只有vi<=vjv_i<=v_j,纔有有貢獻,且貢獻爲xjxix_j-x_i
樹狀數組 +離散化
我們按aia_i從小到大排序,對每個點,我們只需要查詢比aia_i的點且小於等於viv_i的點,那麼答案的貢獻是比他小的點的個數aiSum\cdot a_i-Sum,其中SumSum爲小於等於viv_iaia_i的和值。

struct node{
    int a,b;
}res[N];
bool cmp(node A,node B){
    return A.a < B.a
}
int b[N];ll c[N][2];
int n;
void add(int x,int num){
    for(;x <= n;x += x&(-x)) c[x][0] ++,c[x][1] += num;
}
ll ask(int x,int k){
    ll ans = 0;
    while(x > 0){
        ans+= c[x][k];
        x -= x&(-x);
    }
    return ans;
}
int main(){
    n = read();
    rep(i,1,n) res[i].a = read();
    rep(i,1,n) b[i] = res[i].b = read();
    sort(res+1,res+n+1,cmp);
    sort(b+1,b+n+1);
    int m = unique(b+1,b+n+1) - b - 1;
    ll ans = 0;
    rep(i,1,n){
        int x = lower_bound(b + 1,b +m + 1,res[i].b) - b;
        ans += res[i].a*ask(x,0) - ask(x,1);
        add(x,res[i].a);
    }
    cout << ans;
}

poj2352
題意:在一個二維座標系中存在着若干點,一個點的等級是這個點左下角點的數目。問你從00n1n-1級分別有多少個點。
思路:
這顯然是個二維偏序,我們固定yy軸,按xx軸查詢。即先按yy從小到大排序(yy相同按xx軸從小到大排序),然後按照上面題目的思路,先查詢,在插入。
注意:樹狀數組下標從1開始,所以對於可能出現不合法的情況可以偏移座標軸。

struct node{
    int x,y;
}res[N];
bool cmp(node A,node B){
    if(A.y!=B.y) return A.y < B.y;
    else return A.x < B.x;
}
int c[N];int n;
inline int lowbit(int x) {return x & (-x);}
int ask(int x){
    int ans = 0;
    while(x){
        ans += c[x];
        x -= lowbit(x);
    }
    return ans;
}
void add(int x,int y){
    for(;x <= N;x += lowbit(x)) c[x] += y;
}
int ans[N];
int main(){
    n = read();
    rep(i,1,n) res[i].x = read()+1,res[i].y = read();
    sort(res + 1,res + n +1,cmp);
    rep(i,1,n){
        ans[ask(res[i].x)] ++;
        add(res[i].x,1);
    }
    rep(i,0,n-1) printf("%d\n",ans[i]);
}

P1138 第k小整數

在這裏插入圖片描述
注意:這道題目重複的數不算排名,所以對於每一種數,我們只插入一次就好。直接看代碼吧。

int c[N];
int m;
void add(int x,int y){
    for(;x <= m;x += x & (-x)) c[x] += y;
}
int ask(int x){
    int sum = 0;
    while(x){
        sum += c[x];
        x -= x&(-x);
    }
    return sum;
}
int a[N],b[N];
int kth(int x){//倍增求第k小
    int t = 0;
    for(int i = 19;i>=0;-- i){
        t += 1<<i;
        if(t>m||c[t]>=x) t -= 1<<i;
        else x -= c[t];
    }
    return b[t+1];
}
int main(){
    int n = read(),k = read();
    rep(i,1,n) a[i] = b[i] = read();

    sort(b+1,b+n+1);//離散化
    m = unique(b+1,b+n+1) - b - 1;

    rep(i,1,m) add(i,1);
    if(ask(m)<k) puts("NO RESULT");//看總排名有多少
    else cout << kth(k);
}

編者注:以上爲博主參考諸多網上資料所總結--by K

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章