圖的最大環最長鏈


這是圖中很基本的問題,很多圖的問題可以轉化為求圖中的最大環或最長鏈。
例如Leetcode 5970. 參加會議的最多員工數,等價於求有向圖最長環,和長度為2的環加上其外鏈。

有向圖

最大環

有多種方法:

  • 一種是先用拓撲排序將外鏈去掉,再dfs每一個環
  • 另一種是從某一點出發,記錄途徑的點,如果遇到已經訪問過的點,說明找到了環的入口。減去起始點到入口的距離,就是環的長度。
  • 還有一種有並查集,對於x->y,如果xy同屬於一個集合,說明形成了一個環。

求最小環的方式是類似的。

假設favorite[i]=v 表示從ii有一條邊,這里采用第二種方法

    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。

參考鏈接

  1. 藍橋杯--小朋友崇拜圈(有向圖求最大環)
  2. 019牛客多校第四場A meeting——樹的直徑
  3. 洛谷-P2661 信息傳遞——有向圖中的最小環
  4. Leetcode有向圖最長環+拓撲排序
  5. Leetcode求樹的直徑


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM