1. 程式人生 > 實用技巧 >[COCI2010-2011#7] UPIT

[COCI2010-2011#7] UPIT

[COCI2010-2011#7] UPIT

約定:視\(n,q\)同階

看一下題目的操作

1.區間賦值

2.區間差分加

3.插入元素

4.區間查詢

我們知道1,2操作都是可以用懶標記維護的,具體過程可能有一點細節

1.記錄區間差分加的過程,要記錄等差數列首項和公差,兩個等差數列相加直接首項和公差都相加即可

2.區間賦值的優先順序要高於加法,即打上賦值標記就要清空加法標記,標記下傳時注意先下傳賦值標記

然後具體問題落到如何實現插入元素這個操作上

塊狀連結串列

對於靜態的陣列,可以直接靜態分塊來做

而要動態插入時,找到對應塊,插入即可,但是涉及到編號問題

所以需要每個塊維護一個\(Size\),塊內每個元素維護一個標號\(id_i\)

同時需要對於塊的\(Size\)累字首和\(SumSize\),則塊\(i\)內編號為\(j\)的元素在陣列中的實際編號為\(SumSize_{i-1}+j\)

插入時把整個塊內的元素取出重新標號即可

但是這樣插入後,一個塊的\(Size\)會變大,再實現分塊的操作時複雜度沒有保證

因此需要加入一個操作:當\(Size_i>2\sqrt n\)時,\(O(n)\)重構整個序列,這樣每\(\sqrt n\)次插入操作會導致一次重構,複雜度為均攤的\(O(n\sqrt n)\)

然後可以用類似分塊的方法來直接維護

\[\ \]

線段樹

靜態的操作線段樹可以直接維護

線上段樹上額外維護一個01,表示這個元素是否出現

將插入操作轉化為在讓對應位置的0變為1,但是由於不知道插入後的位置,所以不能直接操作

於是有兩種解決辦法

暴力值域

靜態情況下我們對於\([1,n]\)建樹,但是動態可以對於\([1,n\cdot q]\)建函式式線段樹

離線

離線維護,預處理出插入的位置

\[\ \]

平衡樹

下面是安利時間

來學Treap吧

它可以

1.查詢k大

2.插入元素

3.區間修改

4.區間翻轉

5.可持久化!!

6.吊打Splay

Treap 即樹堆,意思是在滿足二叉查詢樹的性質同時滿足二叉堆的性質

給定每個節點一個額外的隨機權值,讓二叉查詢樹對於這個權值滿足堆的性質即可

這樣構造的二叉查詢樹,樹高是\(O(\log n)\)

帶旋Treap

像普通二叉查詢樹一樣每次插入節點到葉子位置後,可能不滿足二叉堆的性質,因此需要不斷向上zig/zag來調整滿足

區間操作可以嘗試像寫線段樹一樣寫

但是它不可持久化

非旋Treap

維護兩個基礎操作

1.平衡樹合併,操作需要滿足兩棵樹的大小順序確定,返回新的根

2.平衡樹分裂為\([1,d],[d+1,n]\)的兩部分,返回兩棵樹的根

1.合併操作\(x,y\)

按照節點的權值比較誰是平衡樹的根,然後將根的左/右子樹與另一棵樹合併作為新的子樹,遞迴實現

2.分裂\(x,d\)

維護\(Size\)判斷是要分裂左子樹還是右子樹,將子樹分裂得到的部分作為\(x\)新的子樹,遞迴實現即可

typedef pair <int,int> Pii;
#define mp make_pair
int Union(int x,int y) {
	if(!x || !y) return x|y;
	Down(x),Down(y);
	if(key[x]<key[y]) return rs[x]=Union(rs[x],y),Up(x),x;
	return ls[y]=Union(x,ls[y]),Up(y),y;
}

Pii Split(int x,int d){
	if(!x) return mp(x,x);
	if(sz[x]<=d) return mp(x,0);
	if(d==0) return mp(0,x);
	Down(x);
	if(sz[ls[x]]+1<=d) {
		Pii y=Split(rs[x],d-sz[ls[x]]-1);
		return rs[x]=y.first,Up(x),mp(x,y.second);
	} else {
		Pii y=Split(ls[x],d);
		return ls[x]=y.second,Up(x),mp(y.first,x);
	}
}

插入操作可以分裂前\(k\)個,將新節點和得到的兩棵樹按次合併

區間更新可以分裂兩次,將對應區間的子樹操作即可