這是圖中很基本的問題,很多圖的問題可以轉化為求圖中的最大環或最長鏈。
例如Leetcode 5970. 參加會議的最多員工數,等價於求有向圖最長環,和長度為2的環加上其外鏈。
有向圖
最大環
有多種方法:
- 一種是先用拓撲排序將外鏈去掉,再dfs每一個環
- 另一種是從某一點出發,記錄途徑的點,如果遇到已經訪問過的點,說明找到了環的入口。減去起始點到入口的距離,就是環的長度。
- 還有一種有並查集,對於
x->y,如果x和y同屬於一個集合,說明形成了一個環。
求最小環的方式是類似的。
假設favorite[i]=v 表示從i到i有一條邊,這里采用第二種方法
int DirectMaxCycle(vector<int>& favorite) {
int n = favorite.size();
vector<bool> vis(n, false);
int max_cycle = 0;
for(int i = 0;i < n;i++) {
if(vis[i]) continue;
int cur = i;
vector<int> cycle;
while(!vis[cur]) {
vis[cur] = true;
cycle.push_back(cur);
cur = favorite[cur];
}
for(int j = 0;j < cycle.size();j++) {
if(cycle[j] == cur) {
int len = cycle.size() - j;
if(len > max_cycle) max_cycle = len;
break;
}
}
}
return max_cycle;
}
有向無環圖:最長鏈
這里有一個很重要的問題,有環怎么辦?
有環的情況下,求最長鏈是沒有意義的。要么保證無環,要么是求連接到環上的鏈的長度。
例如求連接到環上的鏈的長度,需要從入度為0的節點開始,遞推計算,於是采用拓撲序。
int TopologicalSort(vector<int>& favorite) {
int n = favorite.size();
vector<bool> vis(n, false);
vector<int>in(n, 0);
vector<int>dp(n, 1);
queue<int> q;
for(int i = 0;i < n;i++) in[favorite[i]]++;
for(int i = 0;i < n;i++) {
if(in[i] == 0) q.push(i);
}
while(!q.empty()) {
int cur = q.front();
q.pop();
// cout << cur << " ";
dp[favorite[cur]] = max(dp[favorite[cur]], dp[cur] + 1);
if(--in[favorite[cur]] == 0) q.push(favorite[cur]);
}
// dp[i] 表示到達i的最長鏈的長度
int two_point_sum = 0; // 題目相關部分
for(int i = 0;i < n;i++) {
if(i == favorite[favorite[i]]) two_point_sum += dp[i];
}
return two_point_sum;
}
無向圖
最大環
和有向圖類似,略
無向無環圖:最長鏈
因為是無環圖,求最長鏈也就是求樹的直徑
- 也可以和有向圖一樣,拓撲序+dp
- 還有一種有趣的方法,兩次dfs。可以證明,從任一點出發,dfs能走到的最遠點一定是"直徑"的一個端點,然后從這個端點出發,dfs得到另一個端點。
例如Leetcode310最小數高度,等價於求樹的直徑
第一次dfs找到一個端點,再從這個端點出發dfs找到另一個端點,最后在寫個dfs得到路徑
class Solution {
public:
static const int maxn = 20000+10;
vector<int>graph[maxn];
bool vis[maxn];
int end[2], max_dis=-1;
void dfs(int s, int dis, int flag) {
vis[s] = true;
if(dis >= max_dis) {max_dis = dis; end[flag] = s;}
for(int i = 0; i < graph[s].size(); i++) {
int t = graph[s][i];
if(!vis[t]) {
dfs(t, dis+1, flag);
}
}
}
vector<int>ans;
void path_dfs(int s, int dis, vector<int>& path) {
if(s == end[1]) {
int n = path.size();
// cout << "path: ";
// for(int i = 0; i < n; i++) {
// cout << path[i] << " ";
// }
// cout << endl;
if(n%2 == 0) ans = {path[n/2-1], path[n/2]};
else ans = {path[n/2]};
return;
}
vis[s] = true;
for(int i = 0; i < graph[s].size(); i++) {
int t = graph[s][i];
if(!vis[t]) {
path.push_back(t);
path_dfs(t, dis+1, path);
path.pop_back();
}
}
}
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
memset(vis, 0, sizeof(vis));
dfs(0, 0, 0); // end[0] is rightmost node
memset(vis, 0, sizeof(vis));
dfs(end[0], 0, 1); // end[1] is leftmost node
// cout << end[0] << " " << end[1] << endl;
vector<int>path = {end[0]};
memset(vis, 0, sizeof(vis));
path_dfs(end[0], 0, path);
return ans;
}
};
也可以雙BFS寫法,而且相比前面DFS,BFS可以在求最遠點的時候得到路徑
int bfs(int s){ // 返回距s的最遠點
memset(vis, 0, sizeof(vis));
queue<int>q;
q.push(s);
vis[s] = true;
int u;
while(!q.empty()){
u = q.front();
q.pop();
for(int i = 0; i < graph[u].size(); i++){
int v = graph[u][i];
if(!vis[v]){
vis[v] = true;
q.push(v);
}
}
}
return u;
}
int pre[maxn];
vector<int> path_bfs(int s) { // 返回s到end的路徑
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
queue<int>q;
q.push(s);
vis[s] = true;
int u;
while(!q.empty()){
u = q.front();
q.pop();
for(int i = 0; i < graph[u].size(); i++){
int v = graph[u][i];
if(!vis[v]){
vis[v] = true;
q.push(v);
pre[v] = u;
}
}
}
vector<int>path;
while(u != -1){
path.push_back(u);
u = pre[u];
}
return path;
}
注意
有環圖中,雙dfs/bfs這種方法是錯誤的,很容易找到反例:

圖片來自The time complexity of finding the diameter of a graph
上述方法得到的結果可能是4,而實際是5。
