樹狀數組3種基本操做

告知

本博客是由一個蒟蒻編寫,內容可能出錯,若發現請告訴本蒟蒻,以便大衆閱讀
轉載請註明原網址:http://www.javashuo.com/article/p-ecquzhwr-nv.htmlhtml

樹狀數組和線段樹

衆所周知, 線段樹和樹狀數組是兄弟來的ios

它們之間的關係

樹狀數組能夠解的,線段樹能解
樹狀數組不能夠解的,線段樹仍是能夠解

既然這樣,那我學會線段樹不就搞定了嗎,幹嗎還學樹狀數組呀數組

那麼,樹狀數組優在何處呢?

其實呢,就是碼量少,思惟清晰
對比一下
單點修改區間查詢
線段樹100行起步
樹狀數組呢,50行左右吧
區間修改區間查詢
線段樹估計要飆到150了吧
樹狀數組依舊50行
沒有對比就沒有傷害呀
這時,有些線段樹忠實粉或許會思考人生:你看我還有機會嗎?
機會是有的,那就是,打樹狀數組吧(固然有些題仍是要打線段樹的啦)函數

樹狀數組簡介

樹狀數組圖解

此章節內容部分引用自bestsort的小站
衆所周知,一棵滿二叉樹長樣:
在這裏插入圖片描述
挪一下位置後,變成了這樣
在這裏插入圖片描述
上面這個就是樹狀數組的畫法
準確來講,這時求和數組的畫法
把原數組\(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]\)
對照式子能夠發現,對於一個\(i\)
\(c[i]=a[i-2^k+1]+a[i-2^k+2]+a[i-2^k+3]……+a[i]\)\(k\)爲二進制下\(i\)最低位的1後面的0的個數,例如8對應的\(k\)就等於3,由於\(8_{10}=1000_2\),最低位的1後面有3個0)
這時候,問題就來了,\(2^k\)怎麼求???學習

引入\(lowbit\)

\(lowbit\)函數就是用來求\(2^k\)是多少的
具體操做是spa

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

解釋
「&」這個符號在C++中指的是按位與運算,具體是說,若在二進制下相同的位置兩數都爲1,那麼&出的答案這一位也爲1,不然爲0
例如\(12\&6\)
\(12_{10}=1100_2\)code

\(6_{10}=0110_2\)(空位用0補齊)htm

\(ans=0100_2=4_{10}\)
在上面這個數據中,12和6只有第三個位置上才都是1,那麼答案也就只有這個位置上是1
( 不過學樹狀數組的人應該都不會不知道位運算吧)
那麼\(x\&(-x)\)是什麼意思呢
首先說明\(-x\)在二進制下和\(x\)的關係
在二進制下,\(-x\)就是\(x\)取反後再加1
例如,\(10_{10}=01010_2\),那麼\(-10_{10}=10101_2+1_2=10110_2\)(第一位是符號位)
進行按位與運算後,答案就是\(00010_2=2^1=2_{10}\)(第一位是符號位)
眼睛掃一掃,發現答案就是\(2\)
神奇吧
具體證實呢,我也不會,嘻嘻(畢竟我只是一個蒟蒻)blog

基本應用

1.單點修改,區間查詢

修改

若要更新當前節點的\(a[i]\)
那麼是否是能夠直接更新\(a[i]\)的上級,\(a[i]\)上級的上級,以此類推
\(lowbit\)到上級所在下標圖片

void update(int now,int x)
{
	int i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}

查詢

對於區間查詢,咱們採起前綴和的求法
對於一個區間\([l,r]\),咱們求出\(r\)的前綴和,減去\(l-1\)的前綴和即爲答案
查詢的具體過程呢,也很簡單
就是從要查的節點以此往下,搜索下級
依舊是用\(lowbit\)

int get(int x)
{
	int i,ans;
	ans=0;
	for (i=x;i>=1;i-=lowbit(i))
		ans+=c[i];
	return ans;
}

題目

Loj#130 樹狀數組 1 :單點修改,區間查詢

Code

#include<cstdio>
#include<iostream>
using namespace std;
long long n,m,i,x,y,ch,c[1000005];
long long lowbit(long long x)
{
	return x&(-x);
}
void update(long long now,long long x)
{
	long long i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}
long long get(long long x)
{
	long long i,ans;
	ans=0;
	for (i=x;i>=1;i-=lowbit(i))
		ans+=c[i];
	return ans;
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&x);	
		update(i,x);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&ch,&x,&y);
		if (ch==2) printf("%lld\n",get(y)-get(x-1));
		else update(x,y);
	}
	return 0;
}

2.區間修改,單點查詢

修改

引入差分的思想,記錄數組裏每一個元素與前一個元素的差,那麼\(a_i=\sum_{j=1}^i d_j\),若是修改區間\([l,r]\),令其加上\(x\),那麼\(l\)\(l-1\)的差增長了\(x\)\(r\)\(r+1\)的差減少了\(x\),根據差分,就能夠給\(d_{l}\)加上\(x\),給\(d_{r+1}\)減去\(x\)

查詢

直接根據\(a_i=\sum_{j=1}^i d_j\),查前綴和就好

題目

Loj#131 樹狀數組2:區間修改,單點查詢

Code

#include<cstdio>
using namespace std;
int  n,m,i,l,r,x,bj;
long long a[1000005],c[1000005];
int lowbit(int x)
{
	return x&(-x);
}
void update(int now,int x)
{
	int i;
	for (i=now;i<=n;i+=lowbit(i))
		c[i]+=x;
}
long long get(int x)
{
	int i;
	long long ans;
	ans=0;
	for (i=x;i;i-=lowbit(i))
		ans+=c[i];
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		update(i,a[i]-a[i-1]);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%d",&bj);
		if (bj==1)
		{
			scanf("%d%d%d",&l,&r,&x);
			update(l,x);
			update(r+1,-x);
		}
		else
		{
			scanf("%d",&x);
			printf("%lld\n",get(x));
		}
	}
	return 0;	
}

3.區間修改,區間查詢

這個也是線段樹最麻煩的地方,一般100行起步,但樹狀數組就不用了,實測50行不到,並且我不壓行

先看一下若是按照問題2的方法來求區間前綴和,要怎麼求

位置\(x\)的前綴和=\(\sum_{i=1}^x\sum_{j=1}^id_j\),發如今這個式子裏,\(d_1\)被計算了\(x\)此,\(d_2\)被計算了\(x-1\)次……,\(d_x\)被計算了1次。那麼這個式子就能夠轉化爲

\(\sum_{i=1}^xd_i\times(x-i+1)=(x+1)\sum_{i=1}^xd_i-\sum_{i=1}^xd_i\times i\)

其中\(x+1\)是給出的,那麼咱們記錄\(d_i\)\(d_i\times i\)就能夠了

維護兩個數組\(sum1\)\(sum2\),分別記錄\(d_i\)\(d_i\times i\)

修改

\(sum1\)同問題2的\(d\)\(sum2\)也相似,\(l\)加上\(l\times x\)\(r+1\)減去\((r+1)x\)

查詢

單點\(x\)的前綴和就是\((x+1)\times sum1\)\(x\)的前綴和-\(sum2\)\(x\)的前綴和,區間\([l,r]\)的值就是\(r\)的前綴和-\(l-1\)的前綴和

題目

Loj#132 樹狀數組3:區間修改,區間查詢

Code

#include<cstdio>
using namespace std;
long long n,m,i,l,r,x,bj,a[1000005],c1[1000005],c2[1000005];
long long lowbit(long long x)
{
	return x&(-x);
}
void update(long long k,long long x)
{
	long long i;
	for (i=k;i<=n;i+=lowbit(i))
	{
		c1[i]+=x;
		c2[i]+=x*k;
	}
}
long long get(long long x)
{
	long long i,ans;
	ans=0;
	for (i=x;i;i-=lowbit(i))
		ans+=((x+1)*c1[i])-c2[i];
	return ans;
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for (i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		update(i,a[i]-a[i-1]);
	}
	for (i=1;i<=m;i++)
	{
		scanf("%lld",&bj);
		if (bj==1)
		{
			scanf("%lld%lld%lld",&l,&r,&x);
			update(l,x);
			update(r+1,-x);
		}
		else
		{
			scanf("%lld%lld",&l,&r);
			printf("%lld\n",get(r)-get(l-1));
		}
	}
	return 0;
}

小結

線段樹與樹狀數組有不少類似的地方,可是樹狀數組很明顯的優點就是短,可是線段樹能夠處理不少種狀況,而這裏面有些是樹狀數組作不到的,因此說不管是線段樹仍是樹狀數組,咱們都應該學習一下,而後選擇更好的去解決題目。

不定時更新高階操做

相關文章
相關標籤/搜索