樹狀數組

下面是樹狀數組的學習筆記:ios


目錄:

\(1\))什麼是樹狀數組

\(2\))如何使用樹狀數組

\(3\))樹狀數組的應用


\(1\))什麼是樹狀數組

​ 在解題過程當中,有時候要維護一個數組的前綴\(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)\)


(2)如何使用樹狀數組

​ 接下來就講到實現方面了,樹狀數組的實現是與上面\(4\)個性質分不開的。

第一部分:求\(lowbit(n)\)

\(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
}
第四部分:統計\(A[x]···A[y]\)的值

​ 調用以上的\(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\)啊!


\(3\))樹狀數組的應用

樹狀數組的應用有不少,下面重點將洛谷上兩個模板題講解一下

首先是\(P3374\) 【模板】樹狀數組 \(1\)

【題目描述】

如題,已知一個數列,你須要進行下面兩種操做:

\(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;
}

上面的這道題是典型的樹狀數組「單點修改,區間查詢題」。可是下面一道題就不是這樣了!

\(P3368\) 【模板】樹狀數組 \(2\)

【題目描述】

如題,已知一個數列,你須要進行下面兩種操做:

\(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\)

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息