Graph
Tarjan Algorithm
Tarjan algorithm 常用來判斷一條邊是否是圖中不可替代的連接。這裡以 bridge / critical connection 為例:如果移除某條邊後,原本連通的部分會被切開,那條邊就是 bridge。
Tarjan algorithm 的核心是 DFS order 和 low-link。dfn[u] 表示節點 u 第一次被拜訪的時間,low[u] 表示從 u 的 DFS 子樹出發,能透過 tree edge 或 back edge 到達的最小 dfn。
在無向圖中,若 DFS tree edge cur -> next 滿足 low[next] > dfn[cur],代表 next 的子樹無法透過其他邊回到 cur 或更早的祖先,因此這條邊是 bridge,也就是 critical connection。
時間複雜度
- Tarjan bridge / critical edge:
O(V + E)
模板
C++
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<vector<int>> criticalConnections(
int n,
vector<vector<int>>& connections
) {
vector<vector<pair<int, int>>> adj(n);
for (int i = 0; i < (int)connections.size(); ++i) {
int u = connections[i][0];
int v = connections[i][1];
adj[u].push_back({v, i});
adj[v].push_back({u, i});
}
vector<int> dfn(n, -1);
vector<int> low(n, 0);
vector<vector<int>> ans;
int timer = 0;
auto dfs = [&](auto&& dfs, int cur, int parent_edge) -> void {
dfn[cur] = low[cur] = timer++;
for (auto [next, edge_id] : adj[cur]) {
if (edge_id == parent_edge) continue;
if (dfn[next] == -1) {
dfs(dfs, next, edge_id);
low[cur] = min(low[cur], low[next]);
if (low[next] > dfn[cur]) {
ans.push_back({cur, next});
}
} else {
low[cur] = min(low[cur], dfn[next]);
}
}
};
for (int i = 0; i < n; ++i) {
if (dfn[i] == -1) {
dfs(dfs, i, -1);
}
}
return ans;
}
};
參考題型
- LeetCode - Critical Connections in a Network:把每條無向邊視為 DFS tree edge 或 back edge。若子樹無法回到祖先,該邊就是 critical connection。
Eulerian Path / Circuit
Eulerian path / circuit 適合處理「每條邊都要剛好使用一次」的建構問題。題目不一定會直接說這是 Eulerian path;有時會包成排列、字串或密碼建構,例如要求所有長度為 n 的字串都在答案中出現一次。
實作時常用 Hierholzer algorithm:DFS 時先一路走尚未使用的邊,等目前節點已經沒有可走的邊,再把節點或邊的資訊加入答案。這個「遞迴回來後才加入答案」的 postorder traversal,是這類模板最重要的部分。
時間複雜度
- Hierholzer algorithm:
O(V + E) - Cracking the Safe:
O(k^n)
Postorder Traversal 模板
C++
#include <bits/stdc++.h>
using namespace std;
vector<int> euler_traversal(vector<vector<int>>& adj, int start) {
vector<int> path;
auto dfs = [&](auto&& dfs, int cur) -> void {
while (!adj[cur].empty()) {
int next = adj[cur].back();
adj[cur].pop_back();
dfs(dfs, next);
}
path.push_back(cur);
};
dfs(dfs, start);
reverse(path.begin(), path.end());
return path;
}
若答案要記錄的是邊上的 label,而不是節點,則把 push_back 放在遞迴回來後:
C++
auto dfs = [&](auto&& dfs, int cur) -> void {
for (int x = 0; x < k; ++x) {
int edge = cur * k + x;
if (used.count(edge)) continue;
used.insert(edge);
dfs(dfs, edge % mod);
ans.push_back('0' + x);
}
};
參考題型
- LeetCode - Cracking the Safe:所有長度為
n的密碼都要在答案中出現一次,可以把問題視為 de Bruijn graph 上的 Eulerian circuit。節點代表長度n - 1的 suffix,邊代表長度n的密碼。
C++
class Solution {
public:
string crackSafe(int n, int k) {
int mod = 1;
for (int i = 0; i < n - 1; ++i) {
mod *= k;
}
unordered_set<int> used;
string ans;
auto dfs = [&](auto&& dfs, int cur) -> void {
for (int x = 0; x < k; ++x) {
int edge = cur * k + x;
if (used.count(edge)) continue;
used.insert(edge);
dfs(dfs, edge % mod);
ans.push_back('0' + x);
}
};
dfs(dfs, 0);
ans += string(n - 1, '0');
return ans;
}
};