1. 程式人生 > 實用技巧 >【樹狀陣列·進階篇】樹狀陣列實現平衡樹(樹狀陣列上二分)

【樹狀陣列·進階篇】樹狀陣列實現平衡樹(樹狀陣列上二分)

\(Preface\)

餘未曾想到,樹狀陣列居然是一個這麼高階的東西。

從未想到過樹狀陣列上還可以二分,從未想到過樹狀陣列還能實現平衡樹的功能。

唔姆,樹狀陣列果然也是美麗的,餘喜歡一切美麗的東西。

樹狀陣列上二分

眾所周知,樹狀陣列上的\(a_i\)維護的是\(\sum_{k=i-lowbit(i)+1}^ival_k\)

考慮強行令樹狀陣列的大小為\(2\)的冪

每次二分邊界\([l,r]\),中點就是\(mid\)

然後就會發現,因為樹狀陣列大小是\(2\)的冪,每次二分其實就相當於在判斷二進位制下某一位是否能填作\(1\)

因此,\(a_{mid}\)的值實際上就等於\(\sum_{k=l}^{mid}val_k\)

唔姆,假設樹狀陣列的\(val_i\)表示等於\(i\)的數的個數,那麼\(a_{mid}\)的值等同於值在\([l,mid]\)範圍內的數的個數。

於是就能輕鬆實現第\(k\)大的詢問了。

模板:普通平衡樹

其實只要能夠求第\(k\)大,平衡樹的其他操作都非常簡單了,前驅/後繼都可以轉化為詢問排名再詢問第\(k\)大的操作。

唯一一個特殊注意點,就是由於這道題中值可能為負,需要把每個數都加上\(10^7\)再操作。

程式碼來源恕不透露,\(class\)封裝,碼風毒瘤(儘管在這道題中體現不多)。

友情提醒:I表示inline,可忽略;RI表示register intCI表示const int&

,可以直接視作intW表示while

什麼?問餘為什麼這麼耐心地解釋,卻不直接在程式碼上修改,或者乾脆重寫一份?

當然是因為嫌麻煩啦!

class TreeArray//樹狀陣列實現平衡樹 
{
	private:
		#define V (1<<25)//V儲存總值域,必須為2的冪 
		#define P 10000000//因為存在負數,所有資料都要加上P
		int a[V+5];I void U(RI x,CI v) {W(x<=V) a[x]+=v,x+=x&-x;}//字尾修改 
		I int Q(RI x,RI t=0) {W(x) t+=a[x],x-=x&-x;return t;}//求字首和(小於等於x的數的個數) 
	public:
		I int Kth(RI k) {RI l=1,r=V,mid;W(l^r) a[mid=l+r>>1]<k?(k-=a[mid],l=mid+1):r=mid;return l-P;}//樹狀陣列上二分詢問第k大
		I void Add(CI x) {U(x+P,1);}I void Del(CI x) {U(x+P,-1);}I int Rk(CI x) {return Q(x+P-1)+1;}//插入;刪除;詢問排名 
		I int Pre(CI x) {return Kth(Q(x+P-1));}I int Nxt(CI x) {return Kth(Q(x+P)+1);}//前驅;後繼 
};

[省選聯考 2020 A/B 卷] 冰火戰士

  • 有兩個陣列\(Ice_i,Fire_i\)
  • 每次操作修改某個陣列中某個位置上的數。(始終非負)
  • 每次操作完求一個最大的整數\(k\),使得\(\min\{\sum_{i\le k}Ice_i,\sum_{i\ge k}Fire_i\}\)最大,並求出這個最大值。
  • 運算元量\(\le2\times 10^6\)

很顯然,\(f(k)=\sum_{i\le k}Ice_i\)\(k\)增大遞增,\(g(k)=\sum_{i\ge k}Fire_i\)\(k\)增大遞減。

那麼根據初中函式知識就可以知道,要使最小值最大,就是求兩個函式的交點。

題目很簡單,可惜這道題居然卡線段樹!

這時候就要請出樹狀陣列上二分啦!

每次先二分出最大的\(k\)滿足\(f(k)<g(k)\),由於\(k\)是整數因此不一定能找到交點,還要比較\(f(k)\)\(g(k+1)\)誰更大。

求出最大值之後,還要再次樹狀陣列上二分求出最大的\(k\)

大致思路差不多就這樣啦。

\(Postscript\)

樹狀陣列最大的優點就是程式碼短、常數小,因此真的真的是個非常美麗的演算法哦!