1. 程式人生 > >樹狀陣列 資料結構詳解與模板(可能是最詳細的了)

樹狀陣列 資料結構詳解與模板(可能是最詳細的了)

目錄

單點更新:

區間查詢:

高階操作

求逆序對

操作

原理

查詢

修改

查詢

修改

樹狀陣列基礎

樹狀陣列是一個查詢和修改複雜度都為log(n)的資料結構。主要用於陣列的單點修改&&區間求和.

另外一個擁有類似功能的是線段樹.

具體區別和聯絡如下:

1.兩者在複雜度上同級, 但是樹狀陣列的常數明顯優於線段樹, 其程式設計複雜度也遠小於線段樹.

2.樹狀陣列的作用被線段樹完全涵蓋, 凡是可以使用樹狀陣列解決的問題, 使用線段樹一定可以解決, 但是線段樹能夠解決的問題樹狀陣列未必能夠解決.

3.樹狀陣列的突出特點是其程式設計的極端簡潔性, 使用lowbit技術可以在很短的幾步操作中完成樹狀陣列的核心操作,其程式碼效率遠高於線段樹。

上面出現了一個新名詞:lowbit.其實lowbit(x)就是求x最低位的1;

下面加圖進行解釋

對於一般的二叉樹,我們是這樣畫的

把位置稍微移動一下,便是樹狀陣列的畫法

上圖其實是求和之後的陣列,原陣列和求和陣列的對照關係如下,其中a陣列是原陣列,c陣列是求和後的陣列:

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];

對照式子可以發現  C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k為i的二進位制中從最低位到高位連續零的長度)例如i=8(1000)時,k=3;

C[8] = A[8-2^3+1]+A[8-2^3+2]+......+A[8]

即為上面列出的式子

現在我們返回到lowbit中來

其實不難看出lowbit(i)便是上面的2^k

因為2^k後面一定有k個0

比如說2^5==>100000

正好是i最低位的1加上字尾0所得的值

開篇就說了,lowbit(x)是取出x的最低位1;具體操作為

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

極致簡短!!!!現在我們來理解一下這行程式碼:

我們知道,對於一個數的負數就等於對這個數取反+1

以二進位制數11010為例:11010的補碼為00101,加1後為00110,兩者相與便是最低位的1

其實很好理解,補碼和原碼必然相反,所以原碼有0的部位補碼全是1,補碼再+1之後由於進位那麼最末尾的1和原碼

最右邊的1一定是同一個位置(當遇到第一個1的時候補碼此位為0,由於前面會進一位,所以此位會變為1)

所以我們只需要進行a&(-a)就可以取出最低位的1了

會了lowbit,我們就可以進行區間查詢和單點更新了!!!

--------------------------------------------------------------------------------------------

單點更新:

繼續看開始給出的圖

此時如果我們要更改A[1]

則有以下需要進行同步更新

1(001)        C[1]+=A[1]

lowbit(1)=001 1+lowbit(1)=2(010)     C[2]+=A[1]

lowbit(2)=010 2+lowbit(2)=4(100)     C[4]+=A[1]

lowbit(4)=100 4+lowbit(4)=8(1000)   C[8]+=A[1]

換成程式碼就是:

void update(int x,int y,int n){
    for(int i=x;i<=n;i+=lowbit(i))    //x為更新的位置,y為更新後的數,n為陣列最大值
        c[i] += y;
}

--------------------------------------------------------------------------------------------

區間查詢:

舉個例子 i=5

C[4]=A[1]+A[2]+A[3]+A[4]; 

C[5]=A[5];

可以推出:   sum(i = 5)  ==> C[4]+C[5];

序號寫為二進位制: sum(101)=C[(100)]+C[(101)];

第一次101,減去最低位的1就是100;

其實也就是單點更新的逆操作

程式碼如下:

int getsum(int x){
    int ans = 0;
    for(int i=x;i;i-=lowbit(i))
        ans += c[i];
    return ans;
}

lowbit會了,區間查詢有了,單點更新也有了接下來該做題了

附程式碼:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <string>
#include <vector>
#define For(a,b) for(int a=0;a<b;a++)
#define mem(a,b) memset(a,b,sizeof(a))
#define _mem(a,b) memset(a,0,(b+1)<<2)
#define lowbit(a) ((a)&-(a))
using namespace std;
typedef long long ll;
const int maxn =  5*1e4+5;
const int INF = 0x3f3f3f3f;
int c[maxn];
void update(int x,int y,int n){
    for(int i=x;i<=n;i+=lowbit(i))
        c[i] += y;
}
int getsum(int x){
    int ans = 0;
    for(int i=x;i;i-=lowbit(i))
        ans += c[i];
    return ans;
}
int main()
{
    int t;
    int n;
    int x,y,z;
    string s;
    cin >> t ;
    for(int j=1;j<=t;j++){
        scanf("%d",&n);
        _mem(c,n);      //初始化陣列中前n+1個數為0
        for(int i=1;i<=n;i++){
            scanf("%d",&z);
            update(i,z,n);
        }
        cout <<"Case "<<j<<":"<<endl;
        while(1){
            cin >> s;
            if(s[0] == 'E')
                break;
            scanf("%d%d",&x,&y);
            if(s[0] == 'Q')
                cout << getsum(y)-getsum(x-1)<<endl;
            else if(s[0] == 'A')
                update(x,y,n);
            else
                update(x,-y,n);
        }
    }
    return 0;
}

高階操作

求逆序對

操作

對於陣列a,我們將其離散化處理為b[].區間查詢與單點修改程式碼如下

void update(int p)
{
    while(p<=n)
    {
        a[p] ++;
        p+=lowbit(p);
    }
}

int getsum(int p)
{
    int res = 0;
    while(p)
        res += a[p],p -= lowbit(p);
    return res;
}

a的逆序對個數為:

for(int i=1;i<=n;i++){
    update(b[i]+1);
    res += i-getsum(b[i]+1);
}

res就是逆序對個數,ask,需注意b[i]應該大於0

原理

第一次插入的時候把5這個位置上加上1,read(x)值就是1,當前已經插入了一個數,所以他前面比他大的數的個數就等於 i - read(x) = 1 - 1 = 0,所以總數 sum += 0

第二次插入的時候,read(x)的值同樣是1,但是 i - read(x) = 2 - 1 = 1,所以sum += 1

第三次的時候,read(x)的值是2,i - read(x) = 3 - 2 = 1,所以sum += 1

第四次,read(x)的值是1,i - read(x) = 4 - 1 = 3,所以sum += 3

第五次,read(x)的值是1,i - read(x) = 5 - 1 = 4,所以sum += 4

這樣整個過程就結束了,所有的逆序對就求出來了。

求區間最大值

void Update(int i,int v)
{
    while(i<=maxY)
    {
        t[i] = max(t[i],v);
        i += lowbit(i);
    }
}
int query(int i)
{
    int ans = 0;
    while(i)
    {
        ans = max(ans,t[i]);
        i -= lowbit(i);
    }
    return ans;
}

區間修改+單點查詢

通過“差分”(就是記錄陣列中每個元素與前一個元素的差),可以把這個問題轉化為問題1。

查詢

設原陣列為a[i], 設陣列d[i]=a[i]-a[i-1](a[0]=0),則a[i]=\sum_{j=1}^{i}d[j],可以通過求d[i]的字首和查詢。

修改

當給區間[l,r]加上x的時候,a[l]與前一個元素 a[l-1]的差增加了x,a[r+1]與 a[r]的差減少了x。根據d[i]陣列的定義,只需給a[l]加上 x, 給 a[r+1]減去x即可

void add(int p, int x){ //這個函式用來在樹狀陣列中直接修改
    while(p <= n) sum[p] += x, p += p & -p;
}
void range_add(int l, int r, int x){ //給區間[l, r]加上x
    add(l, x), add(r + 1, -x);
}
int ask(int p){ //單點查詢
    int res = 0;
    while(p) res += sum[p], p -= p & -p;
    return res;
}

區間修改+區間查詢

這是最常用的部分,也是用線段樹寫著最麻煩的部分——但是現在我們有了樹狀陣列!

怎麼求呢?我們基於問題2的“差分”思路,考慮一下如何在問題2構建的樹狀陣列中求字首和:

位置p的字首和 =\sum_{i=1}^{p}a[i]=\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]

在等式最右側的式子\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]中,d[1]被用了p次,d[2]被用了p-1次……那麼我們可以寫出:

位置p的字首和 =\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]=\sum_{i=1}^{p}d[i]*(p-i+1)=(p+1)*\sum_{i=1}^{p}d[i]-\sum_{i=1}^{p}d[i]*i

那麼我們可以維護兩個陣列的字首和:
一個數組是 sum1[i]=d[i]
另一個數組是 sum2[i]=d[i]*i

查詢

位置p的字首和即:(p+1)*sum1陣列中p的字首和 - sum2陣列中p的字首和。

區間[l, r]的和即:位置r的字首和 - 位置l的字首和。

修改

對於sum1陣列的修改同問題2中對d陣列的修改。

對於sum2陣列的修改也類似,我們給 sum2[l] 加上 l * x,給 sum2[r + 1] 減去 (r + 1) * x。

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i)
        sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
    add(l, x), add(r + 1, -x);
}
ll ask(ll p){
    ll res = 0;
    for(int i = p; i; i -= i & -i)
        res += (p + 1) * sum1[i] - sum2[i];
    return res;
}
ll range_ask(ll l, ll r){
    return ask(r) - ask(l - 1);
}

用這個做區間修改區間求和的題,無論是時間上還是空間上都比帶lazy標記的線段樹要優。

二維樹狀陣列

我們已經學會了對於序列的常用操作,那麼我們不由得想到(誰會想到啊喂)……能不能把類似的操作應用到矩陣上呢?這時候我們就要寫二維樹狀陣列了!

在一維樹狀陣列中,tree[x](樹狀陣列中的那個“陣列”)記錄的是右端點為x、長度為lowbit(x)的區間的區間和。
那麼在二維樹狀陣列中,可以類似地定義tree[x][y]記錄的是右下角為(x, y),高為lowbit(x), 寬為 lowbit(y)的區間的區間和。

單點修改+區間查詢

void add(int x, int y, int z){ //將點(x, y)加上z
    int memo_y = y;
    while(x <= n){
        y = memo_y;
        while(y <= n)
            tree[x][y] += z, y += y & -y;
        x += x & -x;
    }
}
void ask(int x, int y){//求左上角為(1,1)右下角為(x,y) 的矩陣和
    int res = 0, memo_y = y;
    while(x){
        y = memo_y;
        while(y)
            res += tree[x][y], y -= y & -y;
        x -= x & -x;
    }
}

區間修改 + 單點查詢

我們對於一維陣列進行差分,是為了使差分陣列字首和等於原陣列對應位置的元素。

那麼如何對二維陣列進行差分呢?可以針對二維字首和的求法來設計方案。

二維字首和:

sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]

那麼我們可以令差分陣列d[i][j]表示a[i][j]與 a[i-1][j]+a[i][j-1]-a[i-1][j-1]的差。

例如下面這個矩陣

 1  4  8
 6  7  2
 3  9  5

對應的差分陣列就是

 1  3  4
 5 -2 -9
-3  5  1

當我們想要將一個矩陣加上x時,怎麼做呢?
下面是給最中間的3*3矩陣加上x時,差分陣列的變化:

0  0  0  0  0
0 +x  0  0 -x
0  0  0  0  0
0  0  0  0  0
0 -x  0  0 +x

這樣給修改差分,造成的效果就是:

0  0  0  0  0
0  x  x  x  0
0  x  x  x  0
0  x  x  x  0
0  0  0  0  0

那麼我們開始寫程式碼吧!

void add(int x, int y, int z){ 
    int memo_y = y;
    while(x <= n){
        y = memo_y;
        while(y <= n)
            tree[x][y] += z, y += y & -y;
        x += x & -x;
    }
}
void range_add(int xa, int ya, int xb, int yb, int z){
    add(xa, ya, z);
    add(xa, yb + 1, -z);
    add(xb + 1, ya, -z);
    add(xb + 1, yb + 1, z);
}
void ask(int x, int y){
    int res = 0, memo_y = y;
    while(x){
        y = memo_y;
        while(y)
            res += tree[x][y], y -= y & -y;
        x -= x & -x;
    }
}

區間修改 + 區間查詢

類比之前一維陣列的區間修改區間查詢,下面這個式子表示的是點(x, y)的二維字首和:

\sum_{i=1}^{x}\sum_{j=1}^{y}\sum_{k=1}^{i}\sum_{h=1}^{j}d[h][k]

(d[h][k]為點(h, k)對應的“二維差分”(同上題))

這個式子炒雞複雜(O(n^4) 複雜度!),但利用樹狀陣列,我們可以把它優化到O(\log_2 n)

首先,類比一維陣列,統計一下每個d[h][k]出現過多少次。d[1][1]出現了x*y次,d[1][2]出現了x*(y-1)次……d[h][k]出現了(x-h+1)*(y-k+1) 次。

那麼這個式子就可以寫成:

\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*(x+1-i)*(y+1-j)

把這個式子展開,就得到:

(x+1)*(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]-(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*i-(x+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*j+\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*i*j

那麼我們要開四個樹狀陣列,分別維護:

d[i][j],d[i][j]*i,d[i][j]*j,d[i][j]*i*j

這樣就完成了!

#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
ll read(){
    char c; bool op = 0;
    while((c = getchar()) < '0' || c > '9')
        if(c == '-') op = 1;
    ll res = c - '0';
    while((c = getchar()) >= '0' && c <= '9')
        res = res * 10 + c - '0';
    return op ? -res : res;
}
const int N = 205;
ll n, m, Q;
ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];
void add(ll x, ll y, ll z){
    for(int X = x; X <= n; X += X & -X)
        for(int Y = y; Y <= m; Y += Y & -Y){
            t1[X][Y] += z;
            t2[X][Y] += z * x;
            t3[X][Y] += z * y;
            t4[X][Y] += z * x * y;
        }
}
void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形
    add(xa, ya, z);
    add(xa, yb + 1, -z);
    add(xb + 1, ya, -z);
    add(xb + 1, yb + 1, z);
}
ll ask(ll x, ll y){
    ll res = 0;
    for(int i = x; i; i -= i & -i)
        for(int j = y; j; j -= j & -j)
            res += (x + 1) * (y + 1) * t1[i][j]
                - (y + 1) * t2[i][j]
                - (x + 1) * t3[i][j]
                + t4[i][j];
    return res;
}
ll range_ask(ll xa, ll ya, ll xb, ll yb){
    return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
}
int main(){
    n = read(), m = read(), Q = read();
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            ll z = read();
            range_add(i, j, i, j, z);
        }
    }
    while(Q--){
        ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();
        if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))
            range_add(xa, ya, xb, yb, a);
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++)
            printf("%lld ", range_ask(i, j, i, j));
        putchar('\n');
    }
    return 0;
}