## 引入 拓撲排序是一種用於有向無環圖(DAG)的排序方法,它能夠將圖中的節點按照一定的順序排列,使得對於任意一條從節點 $u$ 到節點 $v$ 的有向邊,節點 $u$ 在排序結果中出現在節點 $v$ 的前面。換句話說,拓撲排序確保了圖中所有的依賴關係都得到了滿足。由於在一個 DAG 中可能存在多種拓撲排序,因此拓撲排序的結果不一定唯一。 拓撲排序的目標是找到一個節點的順序 $v_1, v_2, \ldots, v_n$,使得對於所有 $i > j$,圖上不存在從 $v_i$ 到 $v_j$ 的路徑。這樣的排序序列可以通過不斷將入度為 0 的節點加入排序序列中,並從圖中刪除這些節點及其相連的邊來實現。
![Image title](./images/136.png){ width="450" }
簡單來說,拓撲排序就是將 DAG 中的節點按照依賴關係的先後順序進行排序的一種算法。 !!! info "拓樸排序判環" 執行拓樸排序,若迴圈結束時判斷已經造訪的結點數是否等於 n。等於 n 說明全部結點都被訪問過,無環;反之,則有環。 ## 同一個 level 的放一起 某些題目會要求一個 level 一個 level 做事情,在這種情況,我們使用兩個 queue,一個存當前 level 的 node,一個存之前 level 的 node,當一層 level 跑完後,再將兩個 queue 的東西互換。 ???+note "code" ```cpp linenums="1" void topo() { queue q; while (q.size()) { queue nq; while(q.size()) { auto u = q.front(); q.pop(); for (auto v : G[u]) { deg[v]--; if (deg[v] == 0) { nq.push(v); } } } q = nq; } } ``` ## 拓樸排序的字典序問題 以下兩個題看似都是字典序問題,但是有差別的。 ???+note "問題一" 給一張 n 點 m 邊 DAG,輸出**字典序最小的**拓樸排序 $n\le 10^5, m\le 2\times 10^5$ ??? note "思路" 同時用 priority queue(小頂堆),這樣就能保證,拓樸序列不唯一時,編號小的優先,即字典序最小。 ???+note "問題二 [CSES - Course Schedule II](https://cses.fi/problemset/task/1757)" 給一張 n 點 m 邊 DAG,輸出拓樸排序,滿足**編號小的盡量靠前** $n\le 10^5, m\le 2\times 10^5$ ??? note "題意解釋"
![Image title](./images/134.png){ width="300" }
很明顯字典序最小的是 [1, 3, 4, 2]。但卻不是本題的答案,因為本題要求「編號小的盡量靠前」,編號 2 還可以提前的,所以 [1, 4, 2, 3] 才是正確的序列。 ??? note "思路"
![Image title](./images/134.png){ width="300" }
看上圖,我們從 1 開始走,鄰接點 3 和 4,我們不知道後面還有個 2,所以不知道 3和 4 先選誰,故正向尋找是錯的。 我們發現從正向考慮沒有辦法考慮到後面所造成的結果。所以我們嘗試反向走,從最後面往前走,優先走編號大的。最後把序列倒著輸出,如此,就滿足了本題。
![Image title](./images/135.png){ width="300" }
另一個例子,助於理解
正確性說明: 走不到 u 的人我一定可以讓它放在 u 後面。某個點要被 pop 掉了,代表走不到他的且比他大的都走完了。所以剩下的只有可能是 - 走的到 u 的 - 走不到 u 但比 u 小的,這時候我們將 u 放在越後面越好 > 參考: ## 例題 ???+note "[CSES - Acyclic Graph Edges](https://cses.fi/problemset/task/1756)" 給一張 n 點 m 邊無向圖,將邊定向使圖是無環的 $n\le 10^5, m\le 2\times 10^5$ ??? note "思路" 根據拓樸排序的性質,我們可以將每條邊都定為小的連到大的方向 ???+note "[2023 全國賽 C. 與自動輔助駕駛暢遊世界 (Autocopilot)](https://sorahisa-rank.github.io/nhspc-fin/2023/problems.pdf#page=11)" 給一張 n 點 m 邊的有向圖,有一個機器人要從起點 s 跟終點 t。當遇到叉路,他會隨機走其中一條,而我們可以花費一個硬幣,可以強制決定他要走的方向,問至少要帶多少錢,才可以保證不會走到死路 $n\le 3000, m\le 3\times 10^4$ ??? note "思路" 【Subtask: 圖是 DAG】 先判斷: - 哪些點是一定走不到 t,dead node - 哪些點是有機率走到 t - 哪些點是一定走得到 t 這可以利用從 out degree = 0 的 node 去 dfs 出去來判斷。之後,我們就可以利用 DAG dp 來看每個點最少要付多少錢。令 dp(u) 從 u 走到終點 t,最少要付多少錢,我們可以列出轉移式: - 如果 u 不可能走到 t, dp(u) = INF - dp(u) = $\min_{u→v} \{$ - 要花錢,$1 + \min\{ dp(v) \}$ - 不花錢 $\max\{ dp(v) \}$ ???+note "[USACO 2013 JAN Party Invitations S](https://www.luogu.com.cn/problem/P3068)" 有 n 頭牛,每頭牛有它自己的朋友圈,沒有一個完全與之相同的。假設該牛朋友圈有 k 頭牛,若已經邀請了 k - 1 頭,那麼剩下的那頭牛也得邀請。問再邀請 1 號牛的情況下,最少需要邀請多少頭乳牛? $n\leq 10^6, \sum k \leq 2.5 \times 10^5$ ??? note "思路" 我們可以把每頭牛想成是一個狀態,利用拓樸排序解決問題。我們利用 vector 存與 i 有關的朋友圈編號,用 set 存朋友圈的集合。 第一次就是將 1 加入 queue,然後循環 vector,把循環到的集合中的 1 都刪去,判斷刪去後的集合大小是否為 1,如果大小是 1,就入隊,重複操作。坑點就是,出來的可能會被重複做,加一個陣列判斷一下是否已經選了這頭奶牛。 ??? note "code" ```cpp linenums="1" #include #include #include #include #include using namespace std; int n, m, ans, vis[1000005]; set s[250005]; vector G[1000005]; queue q; int main() { cin >> n >> m; for (int i = 1; i <= m; i++) { int t; cin >> t; for (int j = 1; j <= t; j++) { int x; cin >> x; G[x].push_back(i); s[i].insert(x); } } q.push(1); vis[1] = 1; while (!q.empty()) { int now = q.front(); q.pop(); ans++; for (int i = 0; i < G[now].size(); i++) { s[G[now][i]].erase(now); if (s[G[now][i]].size() == 1 && !vis[*s[G[now][i]].begin()]) { int t = *s[G[now][i]].begin(); q.push(t); vis[t] = 1; } } } cout << ans << '\n'; return 0; } ``` - JOI 2016 p3 - 2015 nhspc p5 - 2022 toi mock 1 pA - 2021 nhspc pD