算法期中1007. 怪兽训练 (找出有向图中所有的强连通分量的Kosaraju算法)

Description

贝爷的人生乐趣之一就是约战马会长. 他知道马会长喜欢和怪兽对决,于是他训练了N只怪兽,并对怪兽用0到N-1的整数进行编号. 贝爷训练怪兽的方式是让它们一对一互殴. 两只怪兽互殴会发生以下三种可能的结果:
1) 什么事也没发生
2) 第一只怪兽永远消失
3) 第二只怪兽永远消失
怪兽们经过了旷日持久的互殴. 贝爷不知道哪些怪兽进行了互殴也不知道它们互殴的顺序,但他确信无论经过多少次互殴,总有一些怪兽能存活下来,他将要派这些怪兽去和马会长对决. 现在他想知道至少有多少只怪兽能存活下来,你能帮他算出来吗?

请实现下面Solution类中的minLeftMonsters函数,完成上述功能.
参数G: N*N(1 <= N <= 50)字符矩阵,G[i][j]表示怪兽i和怪兽j互殴会发生的结果. 字符‘+’代表怪兽i会消失,’-’代表怪兽j会消失,数字’0’则代表什么都没发生. 输入保证G[i][i]一定是’0’,而且G[i][j]和G[j][i]一定相反(’-’和’+’互为相反,’0’和自身相反).
返回值:怪兽存活的最少数目.

class Solution {
public:
int minLeftMonsters(vector

解法一:

用递归的思路,超时了orz

class Solution {
public:
    int minLeftMonsters(vector<vector<char>> G) {
        int sum = G.size();
        vector<int> rest;
        for (int i = 0; i < sum; i++)  // 最开始所有的怪兽都还存活
            rest.push_back(i);
        int res = findMin(G, rest, sum);
        return res;
    }

    int findMin(vector<vector<char>> G, vector<int> rest, int restNum) {
        int min = restNum;
        int tmpMin = min;
        for (int i = 0; i < restNum; i++) {  // 罗列出两两所有情况,可能会有重复的rest结果
            for (int j = i + 1; j < restNum; j++) {
                vector<int> tmp = rest;
                if (G[i][j] == '+') {  // 这个怪兽会被杀死
                    tmp.erase(tmp.begin() + i);
                    tmpMin = findMin(G, tmp, restNum - 1);
                }
                else if (G[i][j] == '-') {
                    tmp.erase(tmp.begin() + j);
                    tmpMin = findMin(G, tmp, restNum - 1);
                }
                if (tmpMin < min) {
                    min = tmpMin;
                }
            }
        }
        return min;
    }
};

int main() {
    Solution solution;
    vector<vector<char>> matrix1 = { {'0', '+', '-'}, {'-', '0', '+'}, {'+', '-', '0'} };
    vector<vector<char>> matrix2 = { { '0', '0', '0' },{ '0', '0', '0' },{ '0', '0', '0' } };
    int res = solution.minLeftMonsters(matrix1);
    cout << res << endl;
    system("pause");
    return 0;
}

解法二:

抽象成一个有向图,一个怪兽抽象成一个顶点,A怪兽打死B怪兽抽象成A到B的有向边。一个强连通分量经过混战最终只会剩下一个,如果该SCC入度不为0,则该SCC会全军覆灭。一个怪兽(节点)自成一个“强连通分量”,所求结果即图中入度为零的强连通分量的个数。

思路:

  1. 先把参数字符矩阵G转化成两张图:graph和reverseGraph。
  2. 调用KosarajuSCC函数计算出所有入度为0的强连通分量,存储在sccs中。
  3. 统计入度为0的SCC的个数。对每一个强连通分量中的所有节点,看graph中是否有其它节点指向它,若没有,则count++。

Kosaraju算法

本题中最重要的算法就是找出有向图中所有的强连通分量的Kosaraju算法(双深搜)。就是《算法概论》3.4节(p91)的内容。

原理:

如果我们在汇点强连通部件中调用了explore的过程,则我们将恰好获得该强连通部件。在深搜中得到的post值最大的顶点一定位于一个源点强连通部件。那么,对Gr进行深搜,post最大的顶点将来自于Gr中的一个源点强连通部件,相应的也就是G中的汇点强连通部件,按照post值从大到小把各个顶点排序,那么一个SCC中的顶点就在一堆,最前面的一堆就是Gr源SCC中的所有顶点,即graph的汇SCC中所有顶点。栗子如下图所示。

在汇点强连通部件中调用了explore的过程,将恰好获得该强连通部件,因为该汇点SCC不可能去往其它节点了。那么就按照所得节点逆后序在graph执行explored遍历所有节点进行深搜。每调用一次dfs深搜,都得到一个当前图的汇点强连通分量,访问后的节点都标志visited(可以理解为删去),下一次就会访问下一个“汇点强连通分量”。如此可以找出所有的强连通分量。
附赠课本上的图以助理解:

ps: 深度优先搜索函数如下所示:
dfs在函数的最后把该点存储

/*从节点v进行深读搜索*/
    void dfs(int v, vector<vector<int>> graph, vector<int>& order, vector<bool>& visited) {
        visited[v] = true;
        for (int i = 0; i < graph.size(); i++) {
            if (!visited[i] && graph[v][i]) {
                dfs(i, graph, order, visited);
            }
        }
        order.push_back(v);
    } 

步骤:

  1. 在图reverseGraph(Gr)上,调用dfs,得到其逆后序。(注意如果用vector存储,则序列最后一个应当属于GR的源SCC,即图Gr的后序,此时需要把该序列反转一下才是真的逆后序,按dfs的post值从大到小排序,第一个元素属于GR的源SCC;如果用栈逐个压入,那么正好栈顶就是在GR的源SCC中,直接弹出就可以。GR的源SCC就是graph的汇SCC)。
  2. 在图graph上运行深度优先搜索,按照第一步得到的顶点post值的降序(即第一步得到的逆后序)处理每个顶点。外层对每个顶点的循环中每执行的一次完整深搜,都得到一个强连通分量。

代码:

class Solution {
public:
    int minLeftMonsters(vector<vector<char>> G) {
        int count = 0; // 最终的结果
        int sum = G.size();
        // 保存每一个强连通分量
        vector<vector<int>> sccs;
        KosarajuSCC(G, sccs);
        // 统计入度为0的SCC的个数
        for (int i = 0; i < sccs.size(); i++) {
            bool flag = true;
            for (int j = 0; j < sccs[i].size(); j++) {
                for (int k = 0; k < sum; k++) {
                    if (find(sccs[i].begin(), sccs[i].end(), k) != sccs[i].end()) {
                        continue;
                    }
                    //G[i][j]表示怪兽i和怪兽j互殴会发生的结果,
                    //’-’代表怪兽i吃了怪兽j,j会消失
                    //也说明有向图中存在从i到j的边
                    if (G[k][sccs[i][j]] == '-') {
                        flag = false;
                        break;
                    }
                }
                if (flag == false) {
                    break;
                }
            }
            if (flag) {
                count++;
            }
        }
        return count;
    }

    void KosarajuSCC(vector<vector<char>> G, vector<vector<int>>& sccs) {
        int sum = G.size();
        vector<bool> visited(sum, false);
        vector<vector<int>> graph(sum, vector<int>(sum, 0));
        vector<vector<int>> reverseGraph(sum, vector<int>(sum, 0));
        vector<int> reversePostOrder;
        vector<int> postOrder;
        vector<int> components;
        // 构建图的关系矩阵和逆矩阵GR
        for (int i = 0; i < sum; i++) {
            for (int j = 0; j < sum; j++) {
                //G[i][j]表示怪兽i和怪兽j互殴会发生的结果,
                //’-’代表怪兽i吃了怪兽j,j会消失
                if (G[i][j] == '-') {
                    graph[i][j] = 1;
                }
                if (G[i][j] == '+') {
                    reverseGraph[i][j] = 1;
                }
            }
        }
        // 得到逆后序reversePostOrder
            // 先深度搜索,得到的reversePostOrder中的序列最后一个应当属于GR的源SCC,即GR的后续
        for (int i = 0; i < sum; i++) {
            if (!visited[i]) {
                dfs(i, reverseGraph, reversePostOrder, visited);
            }
        }
            // 上述reversePostOrder翻转一下才是真的逆后序,按dfs的post值从大到小排序
            // 第一个属于GR的源SCC
        reverse(reversePostOrder.begin(), reversePostOrder.end());

        // 第二次深度搜索,找出所有的强连通分量SCC
        // 此处的components相当于存储每一次SCC的所有元素
        visited.assign(sum, false);
        for (int i = 0; i < sum; i++) {
            components.clear();
            if (!visited[reversePostOrder[i]]) {
                dfs(reversePostOrder[i], graph, components, visited);
            }
            if(components.size() != 0) {
                sccs.push_back(components);
            }
        }
    }

    /*从节点v进行深读搜索*/
    void dfs(int v, vector<vector<int>> graph, vector<int>& order, vector<bool>& visited) {
        visited[v] = true;
        for (int i = 0; i < graph.size(); i++) {
            if (!visited[i] && graph[v][i]) {
                dfs(i, graph, order, visited);
            }
        }
        order.push_back(v);
    }
};                                 
int main() {
    Solution solution;
    vector> matrix1 = { { '0', '+', '-' },{ '-', '0', '+' },{ '+', '-', '0' } };
    vector> matrix2 = { { '0', '0', '0' },{ '0', '0', '0' },{ '0', '0', '0' } };
    vector> matrix3 = { { '0', '-', '-', '+', '0', '0' },
    { '+', '0', '0', '-', '0', '0' },
    { '+', '0', '0', '-', '-', '0' },
    { '-', '+', '+', '0', '0', '-' },
    { '0', '0', '+', '0', '0', '-' },
    { '0', '0', '0', '+', '+', '0' } };
    vector> matrix4 = { 
    { '0', '-', '+', '0', '0', '0', '0' },
    { '+', '0', '-', '0', '0', '0', '0' },
    { '-', '+', '0', '0', '0', '0', '-' },
    { '0', '0', '0', '0', '-', '+', '0' },
    { '0', '0', '0', '+', '0', '-', '0' },
    { '0', '0', '0', '-', '+', '0', '-' },
    { '0', '0', '+', '0', '0', '+', '0' } };
    int res = solution.minLeftMonsters(matrix3);
    cout << res << endl;
    system("pause");
    return 0;
}
点赞