1. 程式人生 > >淺析樹狀數組(二叉索引樹)及一些模板

淺析樹狀數組(二叉索引樹)及一些模板

一個 程序 時間 cst char .org tin define +=

樹狀數組

  動態連續和查詢問題。給定一個n個元素的數組a1、a2、……,an,設計一個數據結構,支持以下兩種操作:1、add(x,d):讓ax增加d;2、query(l,r):計算al+al+1+…+ar

如何讓query和add都能快速完成呢?方法有很多,這裏介紹的便是樹狀數組。為此我們先介紹lowbit。

  對於正整數x,我們定義lowbit(x)為x的二進制表達式中最右邊的1所對應的值(而不是這個比特的序號)。比如,38288的二進制1001010110010000,所以lowbit(38288)=16(二進制是10000)。在程序中,lowbit(x)=x&-x,計算機裏的整數采用補碼表示,因此-x實際上是x按位取反後末尾加1的結果如下:

38288=1001010110010000

-38288=0110101001110000

二者按位取與後,前面的部分全部變為0,之後lowbit保持不變。接下來看一張圖

技術分享

  對於節點i,如果它是左子節點,那麽他的父節點編號為i+lowbit(i);如果它是右子節點,那麽父節點的編號為i-lowbit(i)。我們設輔助數組C[k]存儲的是從k開始lowbit(k)個元素的和,即C[i]=A[i]+A[i-1]+…+A[i-2^k+1]。

  有了以上預備知識做鋪墊我們就能進行一下操作了!!

一、單點修改+區間查詢

思路:假設修改第i個數即A[i],增量為num,則只需從C[i]開始往右走,沿途修改所有節點對應的C[i](即包含A[i]的區間);而求和sum(i)=A[1]+A[2]+…+A[i],則i到j的和為sum(j)-sum(i-1);

模板題:https://www.luogu.org/problem/show?pid=3374

 1 #include<iostream>
 2 #include<cstdio>
 3 #define maxn 500005
 4 using namespace
std; 5 int a[maxn],b[maxn],n,m; //a為原數組,b為輔助數組 6 inline int getint() //讀入優化 7 { 8 int a=0;char x=getchar();bool f=0; 9 while((x<0||x>9)&&x!=-)x=getchar(); 10 if(x==-)f=1,x=getchar(); 11 while(x>=0&&x<=9){a=a*10+x-0;x=getchar();} 12 return f?-a:a; 13 } 14 void update1(int k,int num) //k為需要修改第幾個數,num為增量 15 { 16 while(k<=n) 17 { 18 b[k]+=num; 19 k+=k&-k; 20 } 21 } 22 int read(int k) //求和 23 { 24 int sum=0; 25 while(k){sum+=b[k];k-=k&-k;} 26 return sum; 27 }; 28 int main() 29 { 30 n=getint(),m=getint(); 31 for(int i=1;i<=n;i++){a[i]=getint();update1(i,a[i]);} //初始化b數組 32 while(m--) 33 { 34 int x,y,z=getint(); 35 if(z==2){x=getint();y=getint();printf("%d\n",read(y)-read(x-1));} //區間求和 36 else {x=getint();y=getint();update1(x,y);} //單點修改 37 } 38 return 0; 39 }

二、區間修改+單點查詢

思路:我們設置輔助數組C[i]=A[i]-A[i-1],容易得出第i個數為sum(i)=C[1]+C[2]+…C[i];至於區間修改,假設修改區間為[i,j]、增量k,我們只需將C[i]+k的同時C[j+1]-k即可

模板題:https://www.luogu.org/problem/show?pid=3368

 1 #include<iostream>
 2 #include<cstdio>
 3 #define maxn 500005
 4 using namespace std;
 5 int a[maxn],b[maxn],n,m;
 6 inline int getint()                   //讀入優化
 7 {
 8     int a=0;char x=getchar();bool f=0;
 9     while((x<0||x>9)&&x!=-)x=getchar();
10     if(x==-)f=1,x=getchar();
11     while(x>=0&&x<=9){a=a*10+x-0;x=getchar();}
12     return f?-a:a;
13 }
14 void update1(int k,int num)   //不想多說了下面都同上一個代碼的註釋,主要是思路不同
15 {
16     while(k<=n)
17     {
18         b[k]+=num;
19         k+=k&-k;
20     }
21 }
22 int read(int k)
23 {
24     int sum=0;
25     while(k){sum+=b[k];k-=k&-k;}
26     return sum;
27 };
28 int main()
29 {
30     n=getint(),m=getint();
31     for(int i=1;i<=n;i++){a[i]=getint();update1(i,a[i]-a[i-1]);}
32     while(m--)
33     {
34         int x,y,z=getint(),q;
35         if(z==2){x=getint();printf("%d\n",read(x));}
36         else {x=getint();y=getint();q=getint();update1(x,q);update1(y+1,-q);}
37     }
38     return 0;
39 }

三、區間修改+區間查詢

思路:(很有趣的數學呵呵~)設置b[i]=a[i]-a[i-1],則有等式:

a[1]+a[2]+...+a[n]

= (b[1]) + (b[1]+b[2]) + ... + (b[1]+b[2]+...+b[n])

= n*b[1] + (n-1)*b[2] +... +b[n]

= n * (b[1]+b[2]+...+b[n]) - (0*b[1]+1*b[2]+...+(n-1)*b[n])

所以我們就維護一個數組c[n],其中c[i] = (i-1)*b[i],每當修改b的時候,就同步修改一下c,這樣復雜度就不會改變那麽原式=n*sigma(b,n) - sigma(c,n)//sigma(b,n)表示b數組前n個數的和(時間復雜度為log2n)

模板:自己找一個(區間修改+區間查詢)線段樹的模板題吧!~~

 1 #include<iostream>
 2 #include<cstdio>
 3 #define maxn 100005
 4 using namespace std;
 5 int a[maxn],b[maxn],c[maxn],n,m;
 6 inline int getint()
 7 {
 8     int a=0;char x=getchar();bool f=0;
 9     while((x<0||x>9)&&x!=-)x=getchar();
10     if(x==-)f=1,x=getchar();
11     while(x>=0&&x<=9){a=a*10+x-0;x=getchar();}
12     return f?-a:a;
13 }
14 void update(int *x,int k,int num)
15 {
16     while(k<=n)
17     {
18         x[k]+=num;
19         k+=k&-k;
20     }
21 }
22 int read(int *x,int k)
23 {
24     int sum=0;
25     while(k){sum+=x[k];k-=k&-k;}
26     return sum;
27 }
28 int main()
29 {
30     n=getint(),m=getint();
31     for(int i=1;i<=n;i++){a[i]=getint();update(b,i,a[i]-a[i-1]);update(c,i,(i-1)*(a[i]-a[i-1]));}
32     while(m--)
33     {
34         int x,y,z=getint(),q;
35         if(z==2){x=getint();y=getint();printf("%d\n",y*read(b,y)-read(c,y)-(x-1)*read(b,x-1)+read(c,x-1));}
36         else {x=getint();y=getint();q=getint();update(b,x,q);update(b,y+1,-q);update(c,x,q*(x-1));update(c,y+1,-q*y);}
37     }
38     return 0;
39 }

四、求逆序數對

思路:了解離散化,它是一種常用的技巧,有時數據範圍太大,可以用來放縮到我們能處理的範圍,必要的是建立一個結構體a[n],v表示輸入的值,order表示原i值,再用一個數組aa[n]存儲離散化後的值
例如:
i:1 2 3 4 5
v: 9 0 1 5 4
排序後:0 1 4 5 9
order:2 3 5 4 1 如果建立映射:aa[a[i].order]=i;
aa:5 1 2 4 3
即原本的9經過排序應該在第5位,現在aa[1]=5,對應原來的9,大小次序不變,只是將9縮小到了5 那麽離散化之後怎麽求逆序對呢?說實在的我這裏想了很久,首先是通過update函數插入一個數,比如update(2,1),一開始都c[n]為0,插入後+1
,現在其余的為0,c[2],c[4]=1,這就說明前面下標為2出有一個數2,這裏是關鍵,c[4]=1不代表下標為4時有一個數4,它的意思是在4之前的區間內所有元素之和是1,即有一個數2,具體的可以看看樹狀圖
然後只有用getsum實時求出插入一個數的前面有幾個數,就可以算出當前小於這個數的數的個數,再通過下標i-getsum(aa[i]),得到大於它的數目,即為逆序數。

模板:POJ2299

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cstdlib>
 5 #include<algorithm>
 6 using namespace std;
 7 const int maxn= 500005;
 8 int aa[maxn];//離散化後的數組
 9 int c[maxn]; //樹狀數組
10 int n;
11 struct Node
12 {
13     int v;
14     int order;
15 }a[maxn];
16 bool cmp(Node a, Node b)
17 {
18     return a.v < b.v;
19 }
20 int lowbit(int k)
21 {
22     return k&(-k); //基本的lowbit函數 
23 }
24 void update(int t, int value)
25 {     //即一開始都為0,一個個往上加(+1),
26     int i;
27     for (i = t; i <= n; i += lowbit(i))
28         c[i] += value;  
29 }
30 int getsum(int t)
31 {  //即就是求和函數,求前面和多少就是小於它的個數
32     int i, sum = 0;
33     for (i = t; i >= 1; i -= lowbit(i))
34         sum += c[i];
35     return sum;
36 }
37 int main()
38 {
39     int i;
40     while (scanf("%d", &n), n)
41     {
42         for (i = 1; i <= n; i++) //離散化
43         {
44             scanf("%d", &a[i].v);
45             a[i].order = i;
46         }
47         sort(a + 1, a + n + 1,cmp);//從1到n排序,cmp容易忘
48         memset(c, 0, sizeof(c));
49         for (i = 1; i <= n; i++)
50             aa[a[i].order] = i;
51         long long ans = 0;
52         for (i = 1; i <= n; i++)
53         {
54             update(aa[i], 1);
55             ans += i - getsum(aa[i]); //減去小於的數即為大於的數即為逆序數
56         }
57         printf("%lld\n", ans);
58     }
59     return 0;
60 }

五、區間最大值

思路:自己yy吧,有點像倍增~~

 1 inline void init()  
 2 {  
 3     CLR(arr,0);  
 4     for(int i=1;i<=N;++i)  
 5         for(int j=i;j<=N&&arr[j]<num[i];j+=lowbit(j))  
 6             arr[j]=num[i];  
 7 }  
 8 inline int query(int L,int R)  
 9 {  
10     int res=0;  
11     for(--L;L<R;){  
12         if(R-lowbit(R)>=L){res=max(res,arr[R]);R-=lowbit(R);}  
13         else{res=max(res,num[R]);--R;}  
14     }  
15     return res;  
16 }  
17 inline void update(int x,int val)  
18 {  
19     int ori=num[x];  
20     num[x]=val;  
21     if(val>=ori)  
22         for(int i=x;i<=N&&arr[i]<val;i+=lowbit(i))  
23             arr[i]=val;  
24     else{  
25         for(int i=x;i<=N&&arr[i]==ori;i+=lowbit(i))  
26         {  
27             arr[i]=val;  
28             for(int j=lowbit(i)>>1;j;j>>=1)  
29                 arr[i]=max(arr[i],arr[i-j]);  
30         }  
31     }  
32 } 

淺析樹狀數組(二叉索引樹)及一些模板