樹狀數組初探


前言

如果你在考提高組的前一天還對這有疑問,那你會與一等獎失之交臂;
如果你還在沖擊普及組一等獎,那這篇博客會浪費你人生中寶貴的5~20分鍾。

(這句話摘自Dijkstra_Liu的blog

概念

樹狀數組(Binary Indexed Tree(B.I.T),Fenwick Tree)是一個查詢和修改都為log(n)的基於倍增思想數據結構(數組)。
樹狀數組和線段樹很像,但能用樹狀數組解決的問題,基本上都能用線段樹解決,而線段樹能解決的樹狀數組不一定能解決。
但相比較而言,樹狀數組效率要高很多,所以在某些題來說,樹狀數組是不二之選。

結構

在oi-wiki上的圖,

思想和線段樹有些類似:用一個大節點表示一些小節點的信息,進行查詢的時候只需要查詢一些大節點而不是更多的小節點。
我們假設父親節點表示它子子孫孫的節點。
列表:

代表 個數
1(0001) 1 1
2(0010) 1 , 2 2
3(0011) 3 1
4(0100) 1 , 2 , 3 , 4 4
5(0101) 5 1
6(0110) 5 , 6 2
7(0111) 7 1
8(1000) 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 8

這里引入一個新函數lowbit(x),即算出x二進制的從右往左出現第一個1以及這個1之后的那些0組成數的二進制對應的十進制的數。
我們不難發現,一個點的代表個數為lowbit(x)

證明:
對於一個x個點,

\[x=a_0*2^0+a_1*2^1+\ldots+a_{upbit(x)}*2^{upbit(x)} \qquad (a_{n}=1 \mid a_{n}=0) \]

在第x個點之前,其必有x-lowbit(x)個點被包含(如上圖)。
所以,第x個點包含lowbit(x)個點。

至於lowbit()的實現,我們可以用x&-x

證明:
你自己推去吧,這里給例子。
例如22,x=10110,x=01001,x+1=01010=-x,x&-x=10110&01010=10
lowbit(22)=2

有了x&-x,我們就可以用O(logn)的復雜度來查詢整個數組。

一維功能

單點修改,區間查詢

\[sum[x]=\sum_{i=1}^{x}a[i] \]

/*O(logn)*/
int t[N];//樹狀數組

void Add(int x,int d)//在第x位加上d
{
      for(;x<=n;x+=(x&-x) t[x]+=d;
}

int Ask(int x)//詢問前x項的和
{
      int ans=0;
      for(;x;x-=(x&-x)) ans+=t[x];
      return ans;
}

Ask(r)-Ask(l-1)//詢問[l,r]

luogu模板

AC code
P3374
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N=5e5+5;
int n,m,c[N];
void Add(int x,int d)
{
	for(;x<=n;x+=(x&-x)) c[x]+=d;
}
int Quest(int x)
{
	int re=0;
	for(;x;x-=(x&-x)) re+=c[x];
	return re;
}
void Solve()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
	{	int a;scanf("%d",&a);Add(i,a); }
	for(int i=1;i<=m;++i)
	{
		int s,a,b;scanf("%d%d%d",&s,&a,&b);
		if(s==1)
			Add(a,b);
		else
			printf("%d\n",Quest(max(b,a))-Quest(min(a,b)-1));
	}
}
int main()
{
	Solve();
	return 0;
}

區間修改,單點查詢

通過差分(就是記錄數組中每個元素與前一個元素的差),把問題轉化為單點修改,區間查詢。

z[i]為i與i-1的差分
查詢\(a[x]=/sum_i=1^xz[i]\)
修改[l,r]+d,即為z[l]+=d,z[r+1]-=d;

/*O(logn)*/
int t[N];

void Add(int x,int d)
{
      for(;x<=n;x+=(x&-x)) t[x]+=d;
}

int Ask(int x)
{
      int ans=0;
      for(;x;x-=(x&-x)) ans+=t[x];
      return ans;
}

Add(l,d),Add(r+1,-d);//修改[l,r]+d

luogu模板

AC code
//P3368
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N=5e5+5;
int n,m,c[N],a[N];
void Add(int x,int d)
{
	for(;x<=n;x+=(x&-x)) c[x]+=d;
}
int Quest(int x)
{
	int re=0;
	for(;x;x-=(x&-x)) re+=c[x];
	return re;
}
void Solve()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
	{	scanf("%d",a+i);Add(i,a[i]-a[i-1]);	}
	for(int i=1;i<=m;++i)
	{
		int s;scanf("%d",&s);
		if(s==1)
		{
			int a,b,c;scanf("%d%d%d",&a,&b,&c);
			Add(a,c);Add(b+1,-c);
		}
		else
		{
			int a;scanf("%d",&a);
			printf("%d\n",Quest(a));
		}
	}
}
int main()
{
	Solve();
	return 0;
}

區間修改,區間查詢

基於區間修改,單點查詢的差分,z[i]為i與i-1的差分。

\[\begin{align*} & \sum_{i=1}^{x}a[i] \\ & = \sum_{i=1}^{x}\sum_{j=1}^{i}z[j] \\ & = \sum_{i=1}^{x}z[j]*(x-i+1) \\ & = (x+1)*\sum_{i=1}^{x}z[i]-\sum_{i=1}^{x}z[i]*i \\ \end{align*} \]

然后,我們可以維護兩個數組的前綴和:
一個是\(t[i]=\sum_{j=1}^{i}z[j]\)
另一個是\(tr[i]=\sum_{j=1}^{i}z[j]*j\)

/*O((logn)^2)*/
int t[N],tr[N];

void Add(int x,int d)
{
      for(int i=x;i<=n;i+=(i&-i))
            t[i]+=d,tr[i]+=d*x;
}

int Ask(int x)
{
      int ans=0;
      for(int i=x;i;i-=(i&-i))
            ans+=(x+1)*t[i]-tr[i];
      return ans;
}

Add(l,d),Add(r+1,-d);//修改[l,r]+d;
Ask(r)-Ask(l-1);//查詢[l,r];

luogu模板

AC code
//P2357
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
const int N=2e5+5;
ll n,m,c[N],c0[N];
void Add(ll x,ll d)
{
	for(ll i=x;i<=n;i+=(i&-i))
		c[i]+=d,c0[i]+=x*d;
}
ll ask(ll x)
{
	ll re=0;
	for(ll i=x;i;i-=(i&-i))
		re+=(x+1)*c[i]-c0[i];
	return re;
}
void Solve()
{
	scanf("%lld%lld",&n,&m);
	int now,last=0;
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&now);
		Add(i,now-last);
		last=now;
	}
	for(int i=1;i<=m;++i)
	{
		ll s;scanf("%lld",&s);
		if(s==1)
		{
			ll a,b,c;scanf("%lld%lld%lld",&a,&b,&c);
			Add(a,c);Add(b+1,-c);
		}
		else if(s==2) 
		{
			ll a;scanf("%lld",&a);
			Add(1,a);Add(2,-a);
		}
		else if(s==3)
		{
			ll a;scanf("%lld",&a);
			Add(1,-a);Add(2,a);
		}
		else if(s==4)
		{
			ll a,b;scanf("%lld%lld",&a,&b);
			printf("%lld\n",ask(max(a,b))-ask(min(a,b)-1));
		}
		else 
		{
			printf("%lld\n",c[1]);
		}
	}
}
int main()
{
	Solve();
	return 0;
}

二維功能

單點修改,區間查詢

\[sum[x][y]=\sum_{i=1}^{x}\sum_{j=1}^{y}a[i][j] \]

/*O(logn*longn)*/
int t[N][N];

void Add(int x,int y,int d)
{
      for(;x<=n;x+=(x&-x))
            for(int i=y;i<=n;i+=(i&-i))
                  t[x][i]+=d;
}

int Ask(int x,int y)
{
      int ans=0;
      for(;x;x-=(x&-x))
            for(int i=y;i<=n;i-=(i&-i)
                  ans+=t[i][j];
      return ans;
}

Ask(x,y)+Ask(a-1,b-1)-Ask(x,a-1)-Ask(b-1,y);//查詢[a,b]~[x][y] (a<=x&&b<=y)

區間修改,單點查詢

因為二維前綴和為

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

所以設z[i][j]a[i][j]a[i-1][j]+a[i][j-1]-a[i-1][j-1]的差。
例如:

a[i][j]
1 4 5 6 3
2 5 3 7 8
9 4 5 6 2
1 4 7 6 9
1 2 3 6 1
z[i][j]
1 3 1 1 -3
1 0 -3 3 4
7 -8 3 -3 -5
-8 8 2 -2 7
0 -2 -2 4 -8

當我們想把中間的3×3加上d時,差分變化為:

z[i][j]
0 00 0 0 00
0 +d 0 0 -d
0 00 0 0 00
0 00 0 0 00
0 -d 0 0 +d

實際變化為:

a[i][j]
0 0 0 0 0
0 d d d 0
0 d d d 0
0 d d d 0
0 0 0 0 0

查詢\(\sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j]\)
修改z[a][b]+=d,z[a][y+1]-=d,z[x+1][b]-=d,z[x+1][y+1]+=d; (a<=x&&b<=y)

/*O((logn)^2)*/
int t[N][N];

void Add(int x,int y,int d)
{
      for(;x<=n;x+=(x&-x))
            for(int i=y;i<=n;i+=(i&-i))
                  t[x][i]+=d;
}

void Ask(int x,int y)
{
      int ans=0;
      for(;x;x-=(x&-x))
            for(int i=y;i;i-=(i&-i))
                  ans+=t[x][i];
      return ans;
}

Add(a,b,d),Add(a,y+1,-d),Add(x+1,b,-d),Add(x+1,y+1,d);//修改[a,b]~[x,y]+d (a<=x&&b<=y)

區間修改,區間查詢

\[\begin{align*} & \sum_{i=1}^{x}\sum_{j=1}^{y}\sum_{q=1}^{i}\sum_{w=1}^{j}z[q][w] \\ & = \sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j]*(x-i+1)*(y-j+1) \\ & = \\ & (x+1)*(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j] \\ & -(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j]*i \\ & -(x+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j]*j \\ & +\sum_{i=1}^{x}\sum_{j=1}^{y}z[i][j]*i*j \end{align*} \]

所以要開四個數組維護:
t[i][j]維護z[i][j]
ti[i][j]維護z[i][j]*i
tj[i][j]維護z[i][j]*j
tij[i][j]維護z[i][j]*i*j

/*O((logn)^2)*/
int t[N][N],ti[N][N],tj[N][N],tij[N][N];

void Add(int x,int y,int d)
{
      for(int i=x;i<=n;i+=(i&-i))
            for(int j=y;j<=n;j+=(j&-j))
                  t[i][j]+=d,ti[i][j]+=d*x,tj[i][j]+=d*y,tij[i][j]+=d*i*j;
}

int Ask(int x,int y)
{
      int ans=0;
      for(int i=x;i;i-=(i&-i))
            for(int j=y;j;j-=(j&-j))
                  ans+=(x+1)*(y+1)*t[i][j]-(y+1)*ti[i][j]-(x+1)*tj[i][j]+tij[i][j];
      return ans;
}

Add(a,b,d),Add(a,y+1,-d),Add(x+1,b,-d),Add(x+1,y+1,d);//修改[a,b]~[x,y]+d (a<=x&&b<=y)
Ask(x,y)+Ask(a-1,b-1)-Ask(x,a-1)-Ask(b-1,y);//查詢[a,b]~[x][y] (a<=x&&b<=y)

拓展功能

不可修改,最大最小

樹狀數組還可以求一個數組的區間中的最大最小。

/*O(logn)*/
int tmax[N],tmin[N],a[N];

memset(tmax,0x80,sizeof(tmax));
memset(tmin,0x3f,sizeof(tmin));

void Add(int x,int d)
{
      for(;x<=n;x+=(x&-x)) 
            tmax[x]=max(tmax[x],d),tmin[x]=min(tmin[x],d);
}

遞歸版本

/*O(logn)*/
int Fmax(int l,int r)
{
      if(l>=r) return a[l];
      if(r-(r&-r)+1>=l) return max(tmax[r],Fmax(l,r-(r&-r)));
      else return max(a[r],Fmax(l,r-1)); 
}

int Fmin(int l,int r)
{
      if(l>=r) return a[l];
      if(r-(r&-r)+1>=l) return min(tmin[r],Fmin(l,r-(r&-r));
      else return min(a[r],Fmin(l,r-1));
}

遞推版本

/*O(logn)*/
int Fmax(int l,int r)
{
      int ans=-0x7fffffff;
      while(l<=r)
      {
            if(r-(r&-r)+1>=l) ans=max(ans,tmax[r]),r-=(r&-r);
            else ans=max(ans,a[r]),--r;
      }
      return ans;
}

int Fmin(int l,int r)
{
      int ans=0x7fffffff;
      while(l<=r)
      {
            if(r-(r&-r)+1>=l) ans=min(ans,tmin[r]),r-=(r&-r);
            else ans=min(ans,a[r]),--r;
      }
      return ans;
}

經驗證明遞推比遞歸快,不信可以試試這題,記得用樹狀數組寫。

AC code
//P3865
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N=1e5+5;
int n,m,a[N],cmax[N];
char S[1<<20], * p1, * p2;
char gc()
{
	if(p1==p2)
	{
		p1=S;
		p2=S+fread(S,1,1<<20,stdin);
	}
	return *p1++;
}
inline int read() 
{
	int s = 0, w = 1;
	char ch = gc();
	while(ch < '0' || ch > '9') {if(ch == '-') w = -1; ch = gc();}
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = gc();
	return s * w;
}
inline int Fmax(int l,int r)
{
    int ans=0;
    while(l<=r)
    {
        if(r-(r&-r)+1>=l) ans=max(ans,cmax[r]),r=r-(r&-r);
        else ans=max(ans,a[r]),r-=1;
    }
	return ans;
}
void Solve()
{
	n=read(),m=read();
	for(register int i=1;i<=n;++i)
	{
		a[i]=read();
		cmax[i]=max(cmax[i],a[i]);
		if(i+(i&-i)<=n)cmax[i+(i&-i)]=cmax[i+(i&-i)]>cmax[i] ? cmax[i+(i&-i)] : cmax[i];
	}
	for(register int i=1;i<=m;++i)
	{
		int a=read(),b=read();
		printf("%d",Fmax(a,b));
		printf("\n");
	}
}
int main()
{
	Solve();
	return 0;
}

區間固定,第k大小

將所有數字看成一個可重集合,即定義數組t[]表示值為x的元素在整個序列重出現了t[x]次。找第k大就是找到最大的x恰好滿足\(\sum_{i=1}^xa[i]<k\)
因為在樹狀數組的結構中,節點是以2的冪的長度划分的,所以我們可以每次擴展2的冪的長度來化簡復雜度。
最后注意第k大小要加1。
這里只列舉第k小,因為第k大為第n-k小。

/*O(logn)*/
int t[N];

void Add(int x,int d)
{
      for(x<=n;x+=(x&-x))t[x]+=d;
}

int Findk(int k)
{
      int ans=0,now=0;
      for(int i=log2(maxn);i>=0;--i)
      {
            ans+=(1<<i);
            if(ans>tot||now+t[ans]>=k) ans-=(1<<i);//擴展失敗
            else now+=t[ans];//擴展成功
      }
      return ans+1;
}

luogu例題

AC code
//P1168
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
typedef long long ll;
using namespace std;
const int N=1e5+5;
int n,m,c[N],a[N],b[N],tot;
void Add(int x,int d)
{
	for(;x<=n;x+=(x&-x)) c[x]+=d;
}
int Findk(int k)
{
	int ans=0,now=0;
	for(int i=log2(n);i>=0;--i)
	{
		ans+=(1<
   
   
   
           
             tot||now+c[ans]>=k) ans-=(1< 
            
              >1)]); } } int main () { Solve(); return 0; } 
             
           

離散化后,帶權數組

有的時候,我們需要用數值做下標,解決這樣的問題就是離散化,也就成了帶權樹狀數組。
這使空間復雜度由T(maxn)變為T(tot)

/*O(nlogn)*/
int n,tot,m[N],a[N];

scanf("%d",&n);
for(int i=1;i<=n;++i)
      scanf("%d",&a[i]),m[i]=a[i];
sort(a+1,a+1+n);
tot=unique(a+1,a+n+1)-a-1;//去重
for(int i=1;i<=n;++i) 
      m[i]=lower_bound(a+1,a+tot+1,m[i])-a;

例如:

a[]
1 2 3 10000
m[]
1 2 3 4

luogu例題

AC code
//P1168
以為沒有?其實和上次是一個題。

結合動規,數組優化

樹狀數組給動規優化,可使O(n)變為O(logn)
以最長子序列為例:

/*O(nlogn)*/
int f[N],a[N],t[N],maxans;

void Add(int x,int d)
{
      for(;x<=n;x+=(x&-x)) t[x]=max(t[x],d);
}

int Fmax(int x)
{
      int ans=0;
      while(l<=r)
      {
            if(r-(r&-r)+1<=l) ans=max(ans,t[r]),r-=(r&-r);
            else ans=max(ans,a[r]),--r;
      }
      return ans;
}

for(int i=1;i<=n;++i)
{
      f[i]=1+Fmax(i);
      Add(i,f[i]);
      maxans=max(maxans,f[i]);
}
printf("%d",maxans);

luogu例題

AC code
//P1637
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
typedef long long ll;
using namespace std;
const int N=3e4+2;
long long n,a[N],ma[N],na,t[N],ans,f[4][N];
void Add(long long  x,long long d)
{
	for(;x<=na;x+=(x&-x)) t[x]+=d;
}
long long Quest(long long x)
{
	long long re=0;
	for(;x;x-=(x&-x)) re+=t[x];
	return re;
}
void Solve()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;++i)
		scanf("%lld",&a[i]),ma[i]=a[i];
	sort(a+1,a+n+1);
	na=unique(a+1,a+n+1)-a-1;
	for(int i=1;i<=n;++i)
		f[1][i]=1,ma[i]=lower_bound(a+1,a+na+1,ma[i])-a;
	for(int i=2;i<=3;++i)
	{
		memset(t,0,sizeof(t));
		for(int j=1;j<=n;++j)
		{
			f[i][j]=Quest(ma[j]-1);
			Add(ma[j],f[i-1][j]);
			if(i==3) ans+=f[i][j];
		}
	}
	printf("%lld",ans);
}
int main ()
{
	Solve();
	return 0;
}

優化技巧

建樹

每一個節點的值是由所有與自己直接相連的兒子的值求和得到的。即每次確定完兒子的值后,用自己的值更新自己的直接父親。
這樣可把O(nlogn)變為O(n)

/*O(n)*/
int a[N],t[N];

for(int i=1;i<=n;++i)
{
      scanf("%d",&a[i]);
      t[i]+=a[i];
      if(i+(i&-i)<=n) t[i+(i&-i)]+=t[i]
}

重建

對付多組數據很常見的技巧。如果每次輸入新數據時,都memset暴力清空樹狀數組,就可能會造成超時。因此使用tag標記,存儲當前節點上次使用時間(即最近一次是被第幾組數據使用)。每次操作時判斷這個位置tag中的時間和當前時間是否相同,就可以判斷這個位置應該是0還是數組內的值。

/*O(logn)*/
int tag[N],t[N],Tag;

void Add(int x,int d)
{
      for(x<=n;x+=(x&-x))
      {
            if(tag[x]!=Tag) t[x]=0,tag[x]=Tag;
            t[x]+=d;
      }
}

void Ask(int x)
{
      int ans=0;
      for(;x;x-=(x&-x))
            if(tag[x]==Tag) ans+=t[x];
      return ans;
}

++Tag;//重建

lougu例題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM