樹狀數組
數據結構知識點1-樹狀數組
樹狀數組的用途就是維護一個數組,重點不是這個數組,而是要維護的東西,最常用的求區間和問題,單點更新。但是某些大牛YY出很多神奇的東西,完成部分線段樹能完成的功能,比如區間更新,區間求最值問題。
樹狀數組當然是跟樹有關了,但是這個樹是怎么構建的吶?這里就不得不感嘆大牛們的腦洞之大了,竟然能想出來用二進制末尾零的個數多少來構建樹以下圖為例:
從上圖能看出來每一個數的父節點就是右邊比自己末尾零個數多的最近的一個,也就是x的父節點就是x+(x&(-x)),這里為什么可以參考計算機位運算,x&(-x)就能得出自己末尾0的個數例如10&(-10)=(0010)二進制。每一個節點保存的就是以他為根節點的數的和,這樣就得出來了更新樹狀數組的函數:
int lowbit(int x) { return x&(-x); } void uodate(int x) { while(x<Max) { c[x]+=val; x+=lowbit; } }
樹狀數組雖然將數據用樹形結構組織起來的但是還是很亂怎么辦吶?實際上樹狀數組維護的是數組的前綴和,比如sum(x)就是a[x]的前綴和,想查詢l~r區間的元素和只需要求出來sum(r)-sum(l-1),這里的sum函數十分的神奇:
int getsum(int x) { int s=0; while(x>0) { s+=c[x]; x-=lowbit; } return s; }
x-(x&(-x))剛巧是前一個棵樹的根節點,這樣就能求出1到x和,以x=9為例,9&(-9)=1;這樣x-(x&(-x))=8,剛巧是前一棵樹的根節點。
例題:poj 2352 stars http://poj.org/problem?id=2352;
/* 題意給你n個星星的坐標,每一個星星的等級為:在不在這個星星右邊並且不比這個星星高的星星的個數 然后出處每個等級星星的個數 樹狀數組,一開始把上一個題的模板扒過來的......真是傷啊,這個題更新點的時候要把右邊的點更新到MAXN要不然會漏掉條件的 */ #include<iostream> #include<stdio.h> #include<string.h> #include<string> #include<algorithm> #define N 32010 using namespace std; int n; int c[N]; int cur[N];//統計每個等級的星星 int lowbit(int x) { return x&(-x); } int getx(int x) { int ans=0; while(x>0) { ans+=c[x]; x-=lowbit(x); } return ans; } void update(int x) { while(x<=N) { c[x]++; x+=lowbit(x); } } int main() { //freopen("in.txt","r",stdin); int x,y; while(scanf("%d",&n)!=EOF&&n) { memset(cur,0,sizeof cur); memset(c,0,sizeof c); for(int i=1;i<=n;i++) { scanf("%d%d",&x,&y); update(x+1); //for(int j=1; j<=n; j++) // cout<<c[j]<<" "; //cout<<endl; //cout<<getx(x+1)-1<<endl; cur[getx(x+1)-1]++; } //cout<<endl; for(int i=0;i<n;i++) printf("%d\n",cur[i]); } return 0; } /* _ooOoo_ o8888888o 88" . "88 (| -_- |) O\ = /O ____/`---'\____ .' \\| |// `. / \\||| : |||// \ / _||||| -:- |||||- \ | | \\\ - /// | | | \_| ''\---/'' | | \ .-\__ `-` ___/-. / ___`. .' /--.--\ `. . __ ."" '< `.___\_<|>_/___.' >'"". | | : `- \`.;`\ _ /`;.`/ - ` : | | \ \ `-. \_ __\ /__ _/ .-` / / ======`-.____`-.___\_____/___.-`____.-'====== `=---=' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ I have a dream!A AC deram!! orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz orz */
然后就是更高層次的操作了,區間更新。區間更新這里引進了一個數組delta數組,delta[i]表示區間 [i, n] 的共同增量,每次你需要更新的時候只需要更新delta數組就行了,因為每段區間更新的數都記錄在這個數組中,那怎么查詢前綴和吶?
sum[i]=a[1]+a[2]+a[3]+......+a[i]+delta[1]*(i-0)+delta[2]*(i-1)+delta[3]*(i-2)+......+delta[i]*(i-i+1); = sigma( a[x] ) + sigma( delta[x] * (i + 1 - x) ) = sigma( a[x] ) + (i + 1) * sigma( delta[x] ) - sigma( delta[x] * x )
紅字不難理解就是從當前位置到i區間共同的增量乘上當前位置到i有多少個數就是增加的總量。
例題 :poj 3468 A Simple Problem with Integers http://poj.org/problem?id=3468
#include<iostream> #include<stdio.h> #include<string.h> #define N 100010 #define ll long long using namespace std; ll c1[N];//delta的前綴和 ll c2[N];//delta * i的前綴和 ll ans[N];//存放的前綴和 ll n,m; string op; ll lowbit(ll x) { return x&(-x); } void update(ll x,ll val,ll *c) { while(x<=n) { c[x]+=val; x+=lowbit(x); } } ll getsum(ll x,ll *c) { ll s=0; while(x>0) { s+=c[x]; x-=lowbit(x); } return s; } int main() { freopen("C:\\Users\\acer\\Desktop\\in.txt","r",stdin); while(scanf("%lld%lld",&n,&m)!=EOF) { memset(c1,0,sizeof c1); memset(c2,0,sizeof c2); memset(ans,0,sizeof ans); for(int i=1;i<=n;i++) { scanf("%lld",&ans[i]); ans[i]+=ans[i-1]; } getchar(); for(int i=1;i<=m;i++) { cin>>op; if(op=="C") { ll s1,s2,s3; scanf("%lld%lld%lld",&s1,&s2,&s3); update(s1,s3,c1);//delta的前綴和 更新 update(s2+1,-s3,c1); update(s1,s1*s3,c2);//delta * i的前綴和 更新 update(s2+1,-(s2+1)*s3,c2); } else if(op=="Q") { /* sigma( a[x] ) + (i + 1) * sigma( delta[x] ) - sigma( delta[x] * x ) */ ll s1,s2; scanf("%lld%lld",&s1,&s2); /*sigma( a[x] )*/ ll cur=ans[s2]-ans[s1-1];//首先等於s1~s2這個區間的基礎值 /*(i + 1) * sigma( delta[x] )*/ cur+=getsum(s2,c1)*(s2+1)-getsum(s2,c2);//0~s2對前綴和的影響 /*sigma( delta[x] * x )*/ cur-=getsum(s1-1,c1)*(s1)-getsum(s1-1,c2);//0~s1對前綴和的影響 printf("%lld\n",cur); } } } }
接着就是RMQ算法,用來求區間最值,直接求當然是不現實的,因為數據很多的時候,復雜度太高,這樣就要先進性預處理,dp[i][j]表示從i開始2^j范圍內的最值,這樣能推出狀態轉移方程 dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1)][j-1])或者min(dp[i][j-1],dp[i+(1<<(j-1)][j-1])。怎么得出來這個方程的吶?就是以i為起點2^j的狀態能由以i為起點到2^j這個范圍的中點2^(j-1)左右兩個部分的最值得到。
首先是預處理部分:
void RMQ_init(int n) { for(int j=1;j<20;j++) for(int i=1;(i+(1<<j)-1)<=n;i++) { dp1[i][j]=max(dp1[i][j-1],dp1[i+(1<<(j-1))][j-1]); dp2[i][j]=min(dp2[i][j-1],dp2[i+(1<<(j-1))][j-1]); } }
然后是查詢
int RMQ(int L,int R) { int k=(int)(log(R-L+1.0)/log(2.0)); return max(dp1[L][k],dp1[R-(1<<k)+1][k]);或者return min(dp2[L][k],dp2[R-(1<<k)+1][k]); }
查詢是什么原理吶?就是l到r的長度內取k的最大值使得2^k<(r-l+1);這樣查詢l到l+2^k內的最值和r-2^k到r內的最值,雖然中間有些元素有些重復但是不會影響正確結果,但是查詢區間和的時候就不能這么重復了。
例題 士兵殺敵(三)http://acm.nyist.net/JudgeOnline/problem.php?pid=119
#include <bits/stdc++.h> #define N 100010 using namespace std; int dp1[N][20];//存放最大值 int dp2[N][20];//存放最小值 int n,m,a; int l,r; void RMQ_init(int n) { for(int j=1;j<20;j++) for(int i=1;(i+(1<<j)-1)<=n;i++) { dp1[i][j]=max(dp1[i][j-1],dp1[i+(1<<(j-1))][j-1]); dp2[i][j]=min(dp2[i][j-1],dp2[i+(1<<(j-1))][j-1]); //cout<<"dp1[i][j]="<<dp1[i][j]<<endl; //cout<<"dp2[i][j]="<<dp2[i][j]<<endl; } } int RMQ(int L,int R) { int k=(int)(log(R-L+1.0)/log(2.0)); //cout<<"max(dp1[L][k],dp1[R-(1<<k)+1][k])="<<max(dp1[L][k],dp1[R-(1<<k)+1][k])<<endl; //cout<<"min(dp2[L][k],dp2[R-(1<<k)+1][k])="<<min(dp2[L][k],dp2[R-(1<<k)+1][k])<<endl; return max(dp1[L][k],dp1[R-(1<<k)+1][k])-min(dp2[L][k],dp2[R-(1<<k)+1][k]); } int main() { //freopen("C:\\Users\\acer\\Desktop\\in.txt","r",stdin); //memset(dp1,0,sizeof dp1); //memset(dp2,0,sizeof dp2); scanf("%d %d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&dp1[i][0]); dp2[i][0]=dp1[i][0]; } RMQ_init(n); for(int i=1;i<=m;i++) { scanf("%d%d",&l,&r); printf("%d\n",RMQ(l,r)); } return 0; }