java數據結構與算法總結(二十二)--樹狀數組

據說樹狀數組就是線段樹。線段樹參考鏈接

有一天,小明給了我三個問題(其實是我自己出的啦~)

 

(1)有一個機器,支持兩種操作,在區間[1,10000]上進行。

操作A:把位置x的值+k

操作B:詢問區間[l,r]所有數字之和

區間的初始值全部爲0

現在你要充當這個機器,操作A和操作B會被穿插着安排給你,

要求對於所有操作B,給出正確的答案。

怎樣做才能最節省精力?


 

(2)有一個機器,支持兩種操作,在區間[1,10000]上進行。

操作A:把區間[l,r]的值全都+x

操作B:詢問x位置的值。

區間的初始值全部爲0

現在你要充當這個機器,操作A和操作B會被穿插着安排給你,

要求對於所有操作B,給出正確的答案。

怎樣做才能最節省精力?


 

(3)有一個機器,支持兩種操作,在區間[1,10000]上進行。

操作A:把區間[l,r]的值全都+x

操作B:詢問區間[l,r]所有數字之和

區間的初始值全部爲0

現在你要充當這個機器,操作A和操作B會被穿插着安排給你,

要求對於所有操作B,給出正確的答案。

怎樣做才能最節省精力?


 

三個問題中操作的數量都可以認爲是10000這個數量級

你可以動用的工具有:無限墨水的筆,一張足夠大的紙,你的大腦(沒多大內存的~)。

注意:

1.舉個例子,進行這種類似的操作:

從一行任意打亂的數字中找一個數字

不能認爲一瞬間就可以找到,在這裏所花費的精力和數字的總數具有線性關係。

2.我們認爲將數據轉換爲二進制不需要任何時間。


 

對於問題1,如果我們每種操作都暴力進行,

那麼顯然總的時間複雜度爲O(mA+n*mB),n表示區間長度,

mA表示操作A執行的次數,mB表示操作B執行的次數。

 

那麼有沒有一種更加輕鬆的辦法呢?

我們將引入一種數據結構,叫做<樹狀數組>。


 

在介紹樹狀數組之前,需要先介紹一下二進制的按位運算,這裏只需要用到兩種:

按位與運算,符號&: 兩個數字都爲1時,得1,否則得0.

那麼1&1=1,0&1=0,1&0=0,0&0=0

 

3&11的值是多少呢?我們把它化成二進制

兩個數字分別爲0011和1011,然後對應的,每位之間進行與運算

  0011

& 1011

——————
  0011

所以答案是0011,即十進制的3。


 

接下來再介紹一下按位非運算,符號~,運算方法是0變1,1變0

比如~9的值,就是~1001(二進制),得到0110。

那麼按位運算就說完了。

最後爲了方便理解後面的內容,還得介紹一個計算機的特點。

 

在計算機中,我們操作的變量通常都有一個固定的位數,

比如c++中 int32_t 類型的變量,它用32位的二進制數來存儲一個整數。

在這個範圍下,

~1=~00000000000000000000000000000001=11111111111111111111111111111110

 

另外,n位的二進制數進行運算,一旦向第n+1位有進位,會直接捨去這個進位,

如四位二進制數1111+0001,答案是0000而不是10000。

 

有了這麼多鋪墊,要開始正題啦~

現在就要介紹一個非常神奇的函數,它叫做lowbit。

 

lowbit(x)=x&((~x)+1) (爲了少引入補碼的概念,我們這裏稍微麻煩了一下,其實x&-x就行)

它的作用是什麼呢?

它只保留"從低位向高位數,第一個數字1"作爲運算結果

比如二進制數00011100,結果就是00000100,也就是4。

11111001,結果就是00000001,也就是1

不信的話可以驗證一下。


 

那麼這種運算對我們的算法有什麼幫助呢?

首先我們來解決一下問題1。

先列舉出從1~32的lowbit,

1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 16 1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 32

 

我們讓第i個位置管理[i-lowbit(i)+1,i]這一段區間,示意圖如下:

怎麼看每個數字管理多少?

只要順着數字往下畫豎線,碰到的第一根橫線所覆蓋的範圍就是它能管理的範圍。

preview

 

我們每次執行操作A(把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行

那麼怎樣快速找到哪些位置能管理到x呢?

答案還是lowbit

 

我們先更新x,然後把x賦給一個新值,x+lowbit(x),那麼新值依然可以管理到x,這樣依次類推直到

x>10000即可。

比如x=2,那麼首先把2的值+k,這不用說。

然後x的新值=x+lowbit(x)=2+lowbit(2)=4,對着上面的示意圖看看,會發現4確實能管理到2,那麼把4的位置+k

然後再來一遍,x=4+lowbit(4)=8,發現8還是能管理到2,繼續給8這個位置+k,就這樣依次類推下去

直到x=16384時,超過10000了,操作完成。

這樣操作之後,樹狀數組裏每一位當前存的值可能並不是該位置的實際值,爲了方便區分,在下文中我們把實際值叫做"原數組的值",當前值就叫做"樹狀數組的值"。

 

可以證明,對於任意一個x屬於[1,10000]我們最多進行log(2,10000)次操作,就可以完成操作A


 

那麼把操作A變複雜(從O(1)變到O(logn))能換來什麼好處?

答案就是,可以把操作B的時間複雜度降低成log級別的


 

詢問區間[L,R]的和sum(L,R)。我們只需要求出sum(1,R)和sum(1,L-1),

然後sum(1,R)-sum(1,L-1)就是sum(L,R)了

那麼對於任意的x,sum(1,x)怎麼求呢?

我們把最終得到的答案存在ans變量中,執行下面的操作:

(1)ans初始化爲0

(2)ans加上x位置的值

(3)給x賦予新值 x-lowbit(x)

(4)如果x>0則跳回操作(2),否則結束算法。

舉個例子介紹一下:

 

一開始我們還是停留在樹狀數組第x位置上(比如x=6吧),答案一開始爲0。

還記得嗎,我們在進行"給原數組第x位置的數增加k"這個操作時,把"能管理到x的所有位置"都增加了k。

那麼,對於任意一個位置,樹狀數組裏的值就是"它能管理到的所有位置上,原數組的值之和"。

因此我們給答案加上樹狀數組第x位置的值,這裏就得到了sum(5,6),因爲6能管理[5,6]

然後給x減去lowbit(x),得到4。再加上x位置的值,也就是sum(1,4),因爲4能管理[1,4]

再讓x=x-lowbit(x),得到0,由於不再大於0,算法終止,得到答案。

這時答案恰好是sum(1,6),哈哈~

依然可以證明,最多只需要進行log級別次數的查詢。

這樣我們進行操作B的時間複雜度也是log級別了。


 

至此,樹狀數組就說完了,問題1也成功得到解決,時間複雜度O((mA+mB)*logn)。

在10000這個數量級下明顯比之前的O(mA+(mB*n))小得多。

而且,位運算的常數非常小,因此整個算法執行速度會很快。


 

問題2怎麼辦?用差分的方法,區間[l,r]所有值+k改成"位置l加上k,位置r+1減去k"

查詢的時候直接查詢sum(1,x)就行,不理解的話可以自己構造一組數據嘗試一下。


 

問題3怎麼辦?稍微複雜一點

用兩個樹狀數組,分別叫做d和s

進行A操作時,d維護差分,s維護x*d[x]。

update(d,l,x);update(d,r+1,-x);

update(s,l,x*l);update(s,r+1,-x*(r+1));

進行B操作時

sum(L,R)=sum(1,R)-sum(1,L-1)

sum(1,L-1)=L*query(d,L-1)-query(s,L-1)

sum(1,R)=(R+1)*query(d,R)-query(s,R)


此方法是從博客看到的,感謝作者,並附上鍊接:

【小結】樹狀數組的區間修改與區間查詢 - 每天心塞一點點 - 博客頻道 - CSDN.NET

 

最後附上我自己封裝的c++樹狀數組模板~

首先是簡潔版,適合比賽現場手寫,非常簡短

int tree[100010],n=100000;
void add(int x,int num)
{
	for(;x<=n;x+=x&-x)
		tree[x]+=num;
} 
int sum(int x)
{
	int answer =0;
	for(;x>0;x-=x&-x)
		answer+=tree[x];
	return answer;
} 

然後是一個功能較全的模板類,可以在項目裏使用(好像並沒有項目用得到2333)

(模板當前版本號爲V1.2)

 

/**
* 樹狀數組模板使用說明
* 以下將樹狀數組維護的區間稱爲原數組
* 操作             說明               時間複雜度     支持範圍
* size()     返回樹狀數組的大小		 O(1)          ~
* resize(x)  重新指定樹狀數組的大小爲x	 O(1)         x>=0
* add(i,v)   將原數組第i位增加v	         O(logn)    0<=i<size
* sum(i)     返回原數組下標從0到i的和	 O(logn)    0<=i<size
* sum(l,r)   返回原數組下標從l到r的和	 O(logn)    0<=l<=r<size
* a[i]       返回原數組下標從0到i的和      O(logn)    0<=i<size
* a[i]+=v    將原數組第i位增加v	         O(logn)    0<=i<size
* a[i]-=v    將原數組第i位減去v		 O(logn)    0<=i<size
* 警告:超出操作支持的範圍,產生的結果無法預料。
* 版權所有,非商業用途可以無限制使用,複製。禁止作爲商業用途使用。
**/
#include<vector>
namespace OrangeOI
{
	template<typename Type>
	class BinaryIndexTree
	{
	private:
		size_t mSize;
		std::vector<Type> mArray;
		struct BinaryIndexTree_Node
		{
			BinaryIndexTree_Node(BinaryIndexTree& bit, size_t pos) :
				mBIT(bit), mPos(pos) {}
			const BinaryIndexTree_Node operator +=(Type value)
			{
				mBIT.add(mPos, value);
				return *this;
			}
			const BinaryIndexTree_Node operator -=(Type value)
			{
				mBIT.add(mPos, -value);
				return *this;
			}
			operator Type()
			{
				return mBIT.sum(mPos);
			}
		private:
			BinaryIndexTree& mBIT;
			size_t mPos;
		};
		int lowbit(int num)
		{
			return num&(~num + 1);
		}
	public:
		BinaryIndexTree() {}
		BinaryIndexTree(size_t size) :
			mSize(size)
		{
			mArray.resize(mSize);
		}
		virtual ~BinaryIndexTree() {}
		const size_t size()
		{
			return mSize;
		}
		void resize(size_t size)
		{
			mSize = size;
			mArray.resize(size);
		}
		void add(int index, Type value)
		{
			for (; index<mSize; index += lowbit(index + 1))
				mArray[index] += value;
		}
		Type sum(int index)
		{
			Type answer = Type();
			for (; index >= 0; index -= lowbit(index + 1))
				answer += mArray[index];
			return answer;
		}
		Type sum(int left, int right)
		{
			return sum(right) - sum(left - 1);
		}
		BinaryIndexTree_Node operator[](size_t pos)
		{
			return BinaryIndexTree_Node(*this, pos);
		}
	};
}

如果大家覺得哪裏講的不是很清楚,歡迎在評論裏提出,我抽空修改,謝謝啦

以上。

來自

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章