晚上在研究怎麼求尤拉圖迴路,看到 \(O(n+m)\) 版本的 HierHolzer 演算法實現,讓我很迷惑。
void dfs(int x){
for(int i = 1;i <= 500; ++i){
if(g[x][i]){
--g[x][i]; --g[i][x];
dfs(i);
}
}
ans[++cnt] = x;
}
OI-Wiki 上對於這段程式碼的描述是這樣的:
將找回路的 DFS 和 Hierholzer 演算法的遞迴合併,邊找回路邊使用 Hierholzer 演算法。
但程式碼中我並沒有直觀地看到“邊找回路邊求環”的過程,而只看到了從一個點出發,有路就往下走的樸素 dfs。所以我就產生了這樣的一個問題:這樣的 dfs 是否能正確地找到尤拉回路呢?
首先根據尤拉圖判別法我們可以知道:尤拉圖中非零度頂點是連通的,且頂點的度數都是偶數。從這個性質出發,我們在腦海中隨便畫出一張尤拉圖,以檢驗演算法的準確性。
接下來開始證明(由於我不會做動畫,所以只能盡我所能直觀地描述了):
我們將這張圖上度數為偶數的節點染為白色,將度數為奇數的節點染為黑色,每經過一條邊就將其刪去並更新顏色。顯然,最開始尤拉圖上都是白色的節點。
任意選取一個點開始我們的樸素 dfs。當我們走過第一條邊的時候,這條邊直接連線的兩個頂點(起點和當前位置)都變成了黑色。當我們繼續向下 dfs 的時候,每走過一條邊,原先位置的黑色節點會變回白色,而當前所在的節點會變為黑色。顯然,在向下 dfs 的過程中,圖上要麼沒有黑點,要麼有且僅有兩個黑點。
當我們訪問的某條邊連線了兩個黑點時,刪去這條邊,整張圖上所有的節點又會都變為白色,此時我們就找到了(刪去了)一個經過起點的環。
到這一步 dfs 並沒有結束,我們也沒有考慮清楚如何記錄答案,但 HierHolzer 已經初具雛形了。如果稱上述過程(從起點向下 dfs 又到了起點)為一次操作,我們可以發現這次操作具有如下的性質:
在一張全是白點的圖上,從任意一個度數非 \(0\) 的節點(度數當然是偶數)出發,必然能找到(刪去)經過這個點的一個環。(注意尤拉圖上偶度節點與“必然”能找到環的聯絡)
繼續 dfs,此時我們正第二次位於起點的位置。回顧一下我們所制定的 dfs 規則:如果有路,就一直往下走。
按照這個規則,如果此時起點的度數大於 \(0\),那麼就繼續往下走。換句話說,也就是重複一次上述的操作。可想而知,每一個經過起點的環都能用上述的方法找到。每經過這樣的一個過程,就會刪去一個經過起點的環,起點的度數就會減 \(2\),直到它變為 \(0\)。
如果此時起點的度數為 \(0\),那麼按照 dfs 的規則,我們就可以開始回溯了。注意此時回溯的順序,如果我們是按照 \(1\to2\to3\to1\) 的順序訪問,那麼我們應該按照 \(1\to3\to2\to1\) 的順序回溯。每回溯到一個節點,這個節點的度數都必然是偶數,那麼我們就可以重複一次操作,刪去一個經過它的環。直到最後回溯到起點,我們就刪去了圖上所有的環。
顯然,在回溯時將當前點新增到答案路徑中,我們就可以將這若干個環拼成一個完整的路徑。而由於這條路徑的起點(所有邊已經都刪光,從起點回溯的時候)和終點(回溯到起點)都是我們所指定的那個起點,所以這條路徑是一條迴路。
綜上,我們就找到了一條符合條件的尤拉回路。
這個過程還可以進行推廣。
比如半尤拉圖上找尤拉通路的問題,就等價於過程中圖上有且僅有兩個黑點的情況。
比如每個環輸出的順序和遍歷的順序相反但起點不變,如果想要字典序最小尤拉回路,就要優先走編號小的節點,最後倒序輸出。
至此我們再回看 HierHolzer 的流程,看似複雜而割裂的“找環——遍歷環——找環”的過程就這樣完美地融入在了一次 dfs 的過程中。
當然,如果有一天你需要脫離模板敲下完整的 HierHolzer,現場推一遍顯然是不現實的。所以我們只需要堅定一個信念:
附模板題 P2731 的 AC 程式碼