图的最大环最长链

这是图中很基本的问题,很多图的问题可以转化为求图中的最大环或最长链。
例如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求树的直径
posted @ 2022-01-02 19:44  Rogn  阅读(2243)  评论(0编辑  收藏  举报