[總結] 並查集相關
[總結] 並查集相關
資料結構進階
優化技巧
路徑壓縮
實現本質是把當前結點接到爺爺上面:
- 遞迴版:從根往下跑。
- 迭代版:從葉子往根跑。
按秩合併
核心是啟發式合併,將小的掛到大的上面,從而達到保持形態和複雜度的目的。
- 按照深度按秩合併
有點像長鏈剖分,可以被卡成 \(\text O(\sqrt{n})\) 的,不建議使用。
但是如果是可撤銷的,寫起來比較方便(深度變化至多為 \(1\))。
- 按照大小按秩合併
類似於重鏈剖分,是均攤 \(\text O(logn)\) 。
邊帶權並查集
邊帶權並查集是通過維護兒子結點與父親結點的可傳遞性關係轉化為兒子結點與根節點的關係
來體現題目要求的。
例題
一共有 \(n\) 列,開始時每一列 \(i\) 有一個編號為 \(i\) 的戰艦,以後會有 \(T\) 個操作:
- 將第 \(i\) 列的所有戰艦按照原來順序加到第 \(j\) 列的所有戰艦後面。
- 詢問兩個戰艦之間的戰艦個數是多少。
採用邊帶權並查集的思想,設 \(d[x]\) 表示 \(x\) 到根節點的邊的數量,則答案為 \(\abs {d[x]-d[y]}-1\) 。
容易發現,這是有傳遞性的。
- 路徑壓縮時的處理
int find(int x){ if(fa[x]==x)return x; int rt=find(fa[x]); d[x]+=d[fa[x]]; return fa[x]=rt; }
一次路徑壓縮時會丟掉父親到根的那一段資訊,或者說本來這個資訊是需要暴力跳 \(father\) 求和的,但是由於路徑壓縮當前結點 \(x\) 接到了根節點上,所以直接求和。
合併時因為是放到第 \(j\) 列後面,需要維護第 \(j\) 列並查集的 \(sz\) 大小。
inline void merge(int x,int y){
x=find(x);y=find(y);
if(x!=y){
fa[x]=y;d[x]+=sz[y];
sz[y]+=sz[x];
}
}
Alice 和 Bob 在玩一個遊戲:他寫一個由 \(0\)
和 \(1\) 組成的序列。Alice 選其中的一段(比如第 \(3\) 位到第 \(5\) 位),問他這段裡面有奇數個 \(1\) 還是偶數個 \(1\)。Bob 回答你的問題,然後 Alice 繼續問。Alice 要檢查 Bob 的答案,指出在 Bob 的第幾個回答一定有問題。有問題的意思就是存在一個 \(01\) 序列滿足這個回答前的所有回答,而且不存在序列滿足這個回答前的所有回答及這個回答。
像這種讓你求解矛盾數量的問題,稱之為悖論問題。
首先第一個轉化,如果一段區間 \([l,r]\) \(1\) 的個數為奇數個,說明 \(sum[l-1]\) 與 \(sum[r]\) 奇偶性不同。
反之,\(sum[l-1]\) 與 \(sum[r]\) 奇偶性相同。
於是我們研究的問題就變成了 \(sum\) 奇偶性是否相同。
這是帶邊權並查集的最經典應用,設 \(d[x]\) 表示 \(x\) 與父親結點奇偶性是否相同,那麼就和上一道題一樣了。
運算改為異或即可。
int get(int x) {
if (x == fa[x]) return x;
int root = get(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = root;
}
總結
邊帶權並查集的做題步驟基本如下:
- 找到可傳遞性關係,即父親與兒子的關係唯一表示。
- 考慮優化:路徑壓縮 \(or\) 按秩合併(保證不會磨滅原資料)。
- 類似地列出狀態之間的真值表,找到合法運算。
分類並查集
同樣是悖論問題,但是由於狀態數由 \(2\) 變為 \(3\),考慮分類並查集。
換句話說,分類並查集和帶邊權並查集本質上是一樣的,只是傳遞資訊的方式不同,思路大體一致。
把它們分為三個域:同類域,捕食域,天敵域。
比如說,\(X\) 吃 \(Y\) 可以轉化為 \(X_{eat}\) 和 \(Y_{self}\) 之間的關係。
總結
並查集就是關係,表示各個點之間能否發生轉化。