併查集と狀態空間搜索
FruitTea
2015 年 9 ⽉ 23 ⽇(⽕曜⽇)
1 Disjoint Sets 互斥集
Introduction
Disjoint Sets,⼜稱互斥集,代表⼀群集合兩兩交集皆為空集合的狀況,常常⽤來處理
「分類」問題。因為功能實⽤、實作簡單,在資訊競賽中很常被使⽤。
實作Disjoint Sets的資料結構我們可以稱她為併查樹 (Union-find Tree)或是併查森林(很 多棵樹嚴格上來說要叫做森林)。基本上我們會以⼀顆併查樹代表⼀個集合。
⼀個基本的併查樹我們必須實作兩種操作,Find和Union。
1.1 Find 查詢
我們有⼀個陣列P[x] 代表 x的 parent是誰,如果 x 是併查樹的樹根(root),我們就把x
的parent當作是⾃⼰。初始狀態下所有的P[x] = x。(⼀開始⼤家都屬於各⾃不同的集合)
如果我們想要知道 x在哪個集合中 (x的root 是誰),我們只要遞迴 P[x]就可以得到答案 了。
Algorithm 1: Find 1 f u n c t i o n Find ( x )
2 i f x ≠ p [ x ] then
3 r e t u r n Find ( p [ x ] )
4 else
5 r e t u r n x
6 end i f
7 end f u n c t i o n
I
可是這樣每次找⼀個點,最慘的狀況下可能會花 O(N) 的時間,所以我們引⼊⼀個稱 為”Path Compression”的概念,速度可以快⾮常多。
Path Compression其實就是在遞迴的過程中,讓所有路徑上的點「直接」連到root。代碼
如下。
Algorithm 2: Find & Path Compression 1 f u n c t i o n Find ( x )
2 i f x ≠ p [ x ] then
3 p [ x ] ← Find ( p [ x ] ) 4 r e t u r n Find ( p [ x ] )
5 else
6 r e t u r n x
7 end i f
8 end f u n c t i o n
1.2 Union 合併
想要合併兩個集合,也就是合併兩顆併查樹,只要把 A樹的樹根接到 B樹的樹根就可以 了。
Algorithm 3: Union 1 f u n c t i o n Union ( x , y )
2 p [ Find ( x ) ] ← Find ( y ) 3 end f u n c t i o n
合併上也有⼀個優化的⽅法叫做”Union By Rank”,也就是把⼩樹合併到⼤樹上,這樣需 要更動的點會⽐較少。
1.3 總結
併查樹可以讓我們在處理分類和合併的問題時很輕鬆的完成。如果我們同時使⽤”Union By Rank”和”Path Compression”,併查樹的複雜度是O(N × α(N)),其中α(N)是阿克曼函 數的反函數。不過就算不⽤”Union By Rank”,複雜度也只是退化到O(N × lg N),在競賽 領域中,我們會遇到的併查樹的操作,α(N) 和lg N 都不會太⼤,所以基本上我們可以把 併查樹的複雜度當作O(N)來使⽤。⾄於”Union By Rank”對於併查樹來說會⽐較⿇煩 (需
要多紀錄rank)⼜不會快很多,所以平常不太需要。
II
1.4 Exercises!!
1. <TIOJ 1312 家族>
給你很多組數字a, b,代表a與b在同個家族。現在給你⼀個數字k,求k所在的家 族中編號最⼩的是誰?
2. <POJ 2492 雌蟲雄蟲>
給你很多對關係x, y,代表蟲x跟蟲y性別不⼀樣。請輸出這些條件是否有任何⽭盾 (也就是有蟲為雌雄同體,ex:(x, y)、(y, z)、(z, x))。
3. <POJ 1182⾷物鏈>
總共有A、B、C三種天使,A吃B,B吃C,C吃A。依序給你很多組關係d, x, y,
若d = 1則代表天使x跟y是同類,d = 2則代表天使x吃y。若當前給的關係與之前
的關係⽭盾,代表此組關係是假的。求總共有多少組關係是假的?
4. <TIOJ 1833 陽炎眩亂>
有⼀群蘿莉機器⼈,分成很多部隊,每個部隊都有⼀個⼥王。題⽬可能要求你合併兩 個部隊或是查詢某隻蘿莉機器⼈隸屬於哪個⼥王。
2 Search 搜尋
Introduction
搜索,暴⼒的找答案,也就是要試過每個可能性後再⼀個⼀個檢查。雖然是暴⼒,但是寫 法好或不好差別還是很⼤的喔!
2.1 Enumeration 枚舉
嘗試所有的可能,然後⼀⼀檢查,找出答案。
2.2 State Space Search 狀態空間搜索
先把問題轉成某種狀態,⽐如你要去學校上學,⼀開始在位置(0, 0)要到位置(s, t)。現在 的位置即為狀態。
⾄於從(0, 0) 到(0, 1)就稱為狀態轉移,⽐如南海路也許就是⼀條可以讓你從家裡轉移到
學校的其中⼀個狀態轉移的路徑。
所有的狀態的集合就是狀態空間,在這裡⾯找答案就是所謂的狀態空間搜索。
我們並不知道這個稱之為狀態空間的迷宮⻑怎麼樣,所以我們必須嘗試⾛⾛看,⾄於怎麼
⾛才有效率,我們介紹幾個常⾒的演算法。
III
2.3 DFS
Depth First Search(深度優先搜索),簡稱DFS。也就是⼀直往前⾛,直到遇到死路就往
後退到上⼀個岔路並且換⼀條⾛,如果所有岔路都⾛完了就再往後退,直到把所有的點都 搜完。跟著下圖的編號順序⾛訪⼀遍就是⼀個DFS 順序的搜尋,當然⼤家的順序可能不 太⼀樣。
實作上,我們只要⽤遞迴就可以輕鬆完成,也可以⽤Stack模擬,可是很⿇煩。並且注意
⼀定要記的紀錄⾛過的點。
Algorithm 4: DFS 1 f u n c t i o n DFS( v )
2 mark v as v i s i t e d
3 f o r a l l u t h a t i s a d j a c e n t t o v do 4 i f u i s n o t v i s i t e d then
5 DFS( u )
6 end i f
7 end f o r
8 end f u n c t i o n
2.4 BFS
Breadth First Search(廣度優先搜索),簡稱BFS,也就是所謂的地毯式搜索。⼀層⼀層的
向外擴⼤搜尋範圍,就像下圖的順序,直到⾛完所有點。當然你不可能⼀次⾛到很多點,
所以下圖的數字代表層數,⾄於同⼀層的順序⼤家可能會不太⼀樣。有⼀個很棒的性質
IV
是,BFS的層數剛好就是起點到該點的距離。
實作BFS也很簡單,只要把DFS的Stack換成Queue就可以了。
Algorithm 5: BFS 1 f u n c t i o n BFS ( v )
2 enqueue v t o Q (Q i s a queue ) 3 mark v as v i s i t e d
4 while Q i s n o t empty do
5 c ← pop Q
6 f o r a l l u t h a t i s a d j a c e n t t o c do 7 i f u i s n o t v i s i t e d then
8 push u t o Q
9 mark u as v i s i t e d
10 end i f
11 end f o r
12 end while
13 end f u n c t i o n
2.5 BiBFS
雙向的 BFS,通常是在知道起點和終點⽽要找最短路徑時,同時交錯在起點和終點做
BFS,起點做⼀層,終點跟著做⼀層。當⼀個點同時被起點和終點搜到時,我們可以確定
⼀定有⼀條最短路徑,從起點經過這個點到終點。
V
BiBFS的好處是,⼀般BFS必須不斷把點存進記憶體裡,假設每個點都連到 N個點,那 麼距離d 的BFS就必須存⼊O(Nd)個點。但是,⽤ BiBFS我們兩個BFS的距離都剩下
d
2,所以只要存O(Nd2)個點,省下了不少記憶體。
2.6 IDDFS
IDDFS就是限制深度的DFS,先做⼀個限制深度的DFS,然後不斷加深深度。這樣做的
原因是避免我們⾛進⼀個⼜深⼜⻑的死路,⽩⽩浪費時間。因為限制了深度,到達終點 時,深度剛好可以代表距離。
也許你會覺得這樣⼀直重複⾛⾛過的點很浪費時間,但是從時間複雜度去分析,假設每個 點都連到N個點,1 d層點數總共有O(Nd+1),⽽d+1層卻也有O(Nd+1)個點,所以總共 只差了常數時間⽽已。
換句話說,IDDFS就是⼀個不會浪費空間的BFS,但是⽐BFS難寫,不過時間複雜度不 變。
2.7 A*
A*就是把BFS 裡的 queue換成 priority queue,也就是依照優先度去搜尋,⽽不是到起
點的實際距離。⾄於優先度是甚麼呢,其實可以⾃⼰定義。A*的優先度是起點到終點的 估計距離,也就是我們希望最短路徑優先被搜尋到。
我們給每⼀點⼀個優先度 f(x) = g(x) + h(x),其中g(x) 代表起點到該點的實際距離,h(x) 代表該點到終點的估計距離。我們只要挑f(x)最⼩的⼈先⾛,很快就可以找到最短路徑。
不過這樣真的會是最短路徑嗎?我們可以證明,因為我們不會⾼估h(x),也就是我們只要 保證該點到終點的實際距離⼤於等於h(x),這樣的話如果我們由⼀條路徑A⾛到終點,另 外⼀條f(x)⽐這條路徑⼤的路徑B,實際距離⼀定⽐路徑 B的 f(x)更⼤,所以路徑 A就 是最短路徑了。
2.8 IDA*
IDA*就是 IDDFS 加上 A*,也就是限制f(x) 的⻑度。當我們做IDDFS 時,深度(剛好就
是A* 裡提到的g(x))超過限制就砍掉。現在我們⼀樣做IDDFS,只是改成 f(x)超過⻑度
限制就砍掉(也就是不⽤g(x)超過限制,如果估計⻑度超過就可以先砍掉了。)
因為我們保證到終點的實際⻑度⼤於等於h(x),所以我們不會不⼩⼼砍掉可以⾛的路徑。
2.9 總結
搜尋其實是很神秘⼜很吃⼈品的領域,透過簡單的DFS、BFS去拓展,想辦法剪枝(就像
IDA*),或是利⽤⼀些性質,甚⾄Random⼀下,都有機會降低複雜度。
VI
之後還會介紹⼀堆神奇的狀態空間搜索,基本上所有的問題都可以透過搜索解出來,把搜 索學好可以幫助你解決很多無論是競賽還是⽣活上的問題。(ex:遊戲中的 bot⾛路可以
⽤A*解決,算⾼維度函數可以⽤爆搜剪枝驗算答案等等。)
附註:因為台灣的⽐賽很雷,常常演算法⽐賽會變成爆搜⽐賽,練好搜尋絕對是好處多於 壞處的。
2.10 Exercises!!
1. <TIOJ 1025 數獨問題>
計算數獨的答案。
2. <TIOJ 1573 15-puzzle>
給你⼀個 4*4的盤⾯,裡⾯有1〜15跟⼀個空格,你每次可以將⼀個與空格相鄰的 數字移到空格上,問最少要幾步才能移動成到指定的盤⾯。
3. <SGU 149 Computer Network>
給你⼀棵樹,問樹的直徑(任兩點最⼤距離)。(N <= 1000000)
4. <SGU 125 Shtirlits>
有⼀個 N × N矩陣 A,每⼀個數字都介於 0〜 9,他給你另⼀個N × N 矩陣 B,Bij
代表 Aij的周圍有幾個⽐他⼤的數字。請輸出任⼀個可⾏的矩陣A,若不存在,則輸 出”NO SOLUTION”。(N <= 3)
5. < 經典問題>
給⼀個 M × N 的格⼦矩形,每個格⼦不是1就是 0,你每次可以對⼀個格⼦進⾏操
作,把那個格⼦和他周圍的格⼦1 變成0、0變成 1,問你⾄少要做幾次才能使所有 格⼦都變成0。(M, N ≤ 15)
6. <TIOJ 1099 射⼿座之⽇>
對⼀個三維空間的座標 (x, y, z),你可以做它兩件事:
(a)把x, y, z 任意調換
(b)把(x, y, z) 變成(2y−x+1, 2x−y−1, z) 問有沒有可能把座標從 (x1, y1, z1)變成(x2, y2, z2)。
7. <TIOJ 1022 跑跑卡恩⾞>
有⼀個⾼低起伏的地圖,你可以在⾼度差不超過5的格⼦之間移動。請問從起點到終 點最短要⾛多遠。
8. <TIOJ 1013 Fire in the forest>
在⼀張給定的地圖中,給你不同的起點和終點,還有⼀個起⽕點,你的速度和⽕的速 度⼀樣,⽽⽕會不斷向四個⽅向延燒。請問你來得急在被⽕燒到前跑到終點嗎?
VII
9. <TIOJ 1226 H 遊戲>
給你⼀個有向無環圖,問你從起點到每個終點的所有可能的路徑的路徑⻑和。
10. <TIOJ 1250 番茄蹲>
有N個⼈排成⼀排,⼀開始都站著,主持⼈可以選擇⼀個數字k,使k的倍數編號的
⼈蹲下,如果已經蹲下就站起來。請你列出經過M個回合後所有可能的狀況。
VIII