[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\)個,將新節點和得到的兩棵樹按次合併
區間更新可以分裂兩次,將對應區間的子樹操作即可