zoukankan      html  css  js  c++  java
  • 强连通分量SCC

    今天听了ztcdl的讲解,队友lkt,cyx带了我几道模板题,突然感觉自己行了(可能自己还没睡醒)


    强连通分量的预备姿势:

    ①树上的DFS序(时间戳):一句话,就是按照dfs的遍历顺序,把每个点再对应一个dfn数组,dfn[i]存的就是dfs序的时间戳。

    ②DFS树:就是在DFS时通向还没有访问过的点的那些边所形成的树。不在树上的边统称为非树边,对于无向图,就只有返祖边;对于有向图,有返祖边、横叉边、前向边。

    黄色的为:返祖边(指向其祖先)

    蓝色的为:前向边(跨过儿子指孙子)

    红色的为:横叉边(指向别的子树)

    ③强联通的概念

    例如:

      图一:所有点都可以走到这个强联通分量中的任意一个点(属于强联通SCC)

      图二:显然不满足SCC

    ④缩点的思想

    在找到强联通之后,我们可以将一个强连通分量视为一个点,从而构造DAG。


    SCC的代码理解:

    我们发现,横叉边会影响判断,所以应该直接删去;前向边不影响答案,可以无视它;只有返祖边才会形成SCC。

    const int maxn = 1e4 + 10;
    vector<int>Map[maxn];
    vector<int>a[maxn];
    int n, m;
    int low[maxn];      //low[i]表示从i出发能够回到的最远的祖先
    int sccno[maxn];    //sccno[i]表示i属于第sccno[i]个强连通分量
    int dfn[maxn];      //DFS序就是DFS时某个点是第几个被访问的,用dfn[i]表示i的时间戳,
    int dfs_clock;      //时间戳
    int scc_cnt;        //强连通分量的个数
    stack<int>S;
    int num[maxn];      //num[i]表示第i个强连通分量中存在多少点
    void tarjan(int u)  //dfs(u)结束后 low[u]、pre[u]将会求出
    {
        dfn[u] = low[u] = ++dfs_clock;
        S.push(u);
        for (int i = 0; i < Map[u].size(); i++)//u->v
        {
            int v = Map[u][i];
            if (!dfn[v])               //说明v还未被dfs
            {
                tarjan(v);             //会自动求出low[v]、dfn[v]
                low[u] = min(low[u], low[v]);
            }
            else if (!sccno[v])        //说明v正在dfs:只求出了dfn[v]
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u])          //说明找到了一个强连通分量
        {
            scc_cnt++;
            for (;;)
            {
                int x = S.top(); S.pop();
                sccno[x] = scc_cnt;
                num[scc_cnt]++;
                a[scc_cnt].push_back(x);
                if (x == u)break;
            }
        }
    }
    
    void find_scc()
    {
        dfs_clock = scc_cnt = 0;
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(sccno, 0, sizeof(sccno));
        for (int i = 1; i <= n; i++)
            if (!dfn[i])              //dfn[i] == 0 说明还没走第i个点
                tarjan(i);
    }

    可以手模一下下图:

    先应该将边3->4,5->6直接删去

    之后,初始化栈为empty

    ①1进入,dfn[1]=low[1]=++cnt=1   栈:1

    ②1->2 dfn[2]=low[2]=++cnt=2    栈: 1 2

    ③2->4 dfn[4]=low[4]=++cnt=3    栈: 1 2 4

    ④4->6 dfn[6]=low[6]=++cnt=4    栈: 1 2 4 6

    6无出度,dfn[6]==low[6],说明6是SCC的根节点

    回溯到4后发现4找到了一个已经在栈中的点1,更新low[4],于是 low[4]=1

    由4继续回到2 low[2]=1;

    由2继续回到1 low[1]=1;

    另一支,low[5]=dfn[5]=6;

    由5继续回到3 low[3]=5;

    由3继续回到1 low[1]=1;

    画图更快:(橙色为dfn,蓝色为low)


    例题:POJ1236 Network of Schools

    题面:

    题意:输入N行,第i行就表示,i与第i行中的所有数字x有一条i->x的边,每行以0结尾,第一行输出至少发几次能让所有学校收到软件;第二行输出如果只发一次,还需要添加几条线路。

    题解:裸题,题A直接跑tarjan即可,缩点后,需要向所有入度为0的点发送信息

    题B,统计一下缩完点后入度为0,出度为0的点的个数,贪心策略——入度为0的点和出度为0的点相连,答案为:max(入度0,出度0)

    需要注意,特判天然是一个SCC的情况(此情况题B无需加边,如果不特判会被看成max(出度0,入度0)=1)。

    #include <iostream> 
    #include <stack>
    #include <vector>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    typedef long long ll;
    const int maxn = 1e4 + 10;
    vector<int>Map[maxn];
    vector<int>a[maxn];
    int n, m;
    int low[maxn];      //low[i]表示从i出发能够回到的最远的祖先
    int sccno[maxn];    //sccno[i]表示i属于第sccno[i]个强连通分量
    int dfn[maxn];      //DFS序就是DFS时某个点是第几个被访问的,用dfn[i]表示i的时间戳,
    int dfs_clock;      //时间戳
    int scc_cnt;        //强连通分量的个数
    
    stack<int>S;
    int num[maxn];      //num[i]表示第i个强连通分量中存在多少点
    void tarjan(int u)  //dfs(u)结束后 low[u]、pre[u]将会求出
    {
        dfn[u] = low[u] = ++dfs_clock;
        S.push(u);
        for (int i = 0; i < Map[u].size(); i++)//u->v
        {
            int v = Map[u][i];
            if (!dfn[v])               //说明v还未被dfs
            {
                tarjan(v);             //会自动求出low[v]、dfn[v]
                low[u] = min(low[u], low[v]);
            }
            else if (!sccno[v])        //说明v正在dfs:只求出了dfn[v]
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u])          //说明找到了一个强连通分量
        {
            scc_cnt++;
            for (;;)
            {
                int x = S.top(); S.pop();
                sccno[x] = scc_cnt;
                num[scc_cnt]++;
                a[scc_cnt].push_back(x);
                if (x == u)break;
            }
        }
    }
    void find_scc()
    {
        dfs_clock = scc_cnt = 0;
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(sccno, 0, sizeof(sccno));
        for (int i = 1; i <= n; i++)
            if (!dfn[i])              //dfn[i] == 0 说明还没走第i个点
                tarjan(i);
    }
    int in[maxn];//记录重构图后的入度
    int out[maxn];//记录重构图后的出度
    void init(void)
    {
        memset(num, 0, sizeof(num));
        memset(in, 0, sizeof(in));
        memset(out, 0, sizeof(out));
        for (int i = 0; i < maxn; i++)
            Map[i].clear(), a[i].clear();
    }
    int main()
    {
        cin >> n;
        init();
        for (int i = 1; i <= n; i++)
        {
            int k;
            while (cin >> k)
            {
                if (k == 0)break;
                Map[i].push_back(k);
            }
        }
        find_scc();//找出所有的强连通分量
        //缩点
        for (int u = 1; u <= n; u++)
        {
            for (int i = 0; i < Map[u].size(); ++i)//for(int i=0;i<Map[u].size();++i){v=Map[u][i]}
            {
                int v = Map[u][i];
                if (sccno[u] == sccno[v])
                    continue;
                in[sccno[v]]++;
                out[sccno[u]]++;
            }
        }
        if (scc_cnt == 1)
            cout << 1 << endl << 0 << endl;
        else 
        {
            ll ans = 0;
            int in_n = 0, out_n = 0;
            for (int i = 1; i <= scc_cnt; i++)
            {
                if (in[i] == 0)
                    in_n++;
                if (out[i] == 0)
                    out_n++;
            }
            cout << in_n <<endl;
            cout << max(in_n, out_n) << endl;
        }
        return 0;
    }

    例题:HDU1269 迷宫城堡

    题面:

    题意:找是否存在一个SCC满足包含所有点

    题解:裸题,直接跑tarjan即可,法①判断是否只有一个SCC;法②判断是否存在n个点的SCC

    代码:

    #include <iostream> 
    #include <stack>
    #include <vector>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int maxn = 1e4 + 10;
    vector<int>Map[maxn];
    vector<int>a[maxn];
    int n, m;
    int low[maxn];       //low[i]表示从i出发能够回到的最远的祖先
    int sccno[maxn];     //sccno[i]表示i属于第sccno[i]个强连通分量
    int dfn[maxn];       //DFS序就是DFS时某个点是第几个被访问的,用dfn[i]表示i的时间戳,
    int dfs_clock;       //时间戳
    int scc_cnt;         //强连通分量的个数
                        
    stack<int>S;
    int num[maxn];//num[i]表示第i个强连通分量中存在多少点
    void tarjan(int u)//dfs(u)结束后 low[u]、pre[u]将会求出
    {
        dfn[u] = low[u] = ++dfs_clock;
        S.push(u);
        for (auto v : Map[u])//u->v
        {
            if (!dfn[v])//说明v还未被dfs
            {
                tarjan(v);//会自动求出low[v]、dfn[v]
                low[u] = min(low[u], low[v]);
            }
            else if (!sccno[v])//说明v正在dfs:只求出了dfn[v]
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u])//说明找到了一个强连通分量
        {
            scc_cnt++;
            for (;;)
            {
                int x = S.top(); S.pop();
                sccno[x] = scc_cnt;
                num[scc_cnt]++;
                a[scc_cnt].push_back(x);
                if (x == u)break;
            }
        }
    }
    void find_scc()
    {
        dfs_clock = scc_cnt = 0;
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(sccno, 0, sizeof(sccno));
        for (int i = 1; i <= n; i++)
            if (!dfn[i])//dfn[i] == 0 说明还没走第i个点
                tarjan(i);
    }
    int main()
    {
        while (cin >> n >> m)
        {
            if (n == 0 && m == 0)break;
            memset(num, 0, sizeof(num));
            for (int i = 0; i < maxn; i++)
                Map[i].clear(), a[i].clear();
            for (int i = 1; i <= m; i++)
            {
                int u, v;
                cin >> u >> v;
                Map[u].push_back(v);
            }
            find_scc();//找出所有的强连通分量
            int flag = 0;
            for (int i = 1; i <= scc_cnt; i++)
            {
                if (num[i] == n)
                    flag = 1;
            }
            if (flag == 1)
                cout << "Yes" << endl;
            else
                cout << "No" << endl;
        }
        return 0;
    }

    例题:P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G

    题面:

    题意:缩点后找出度为0的点是否唯一,如果唯一,则认为是这个点的整体是受欢迎的,直接输出这个(可能缩过的)点包含的总点数。

    如果在缩点后有多个出度为0的点,显然不是在一条链上,一定不符合题意。

    代码:

    #include <iostream> 
    #include <stack>
    #include <vector>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int maxn = 1e4 + 10;
    vector<int>Map[maxn];
    int n, m;
    int low[maxn];   //low[i]表示从i出发能够回到的最远的祖先
    int sccno[maxn]; //sccno[i]表示i属于第sccno[i]个强连通分量
    int dfn[maxn];   //DFS序就是DFS时某个点是第几个被访问的,用dfn[i]表示i的时间戳
    int dfs_clock;   //时间戳
    int scc_cnt;     //强连通分量的个数
    
    stack<int>S;
    int num[maxn];//num[i]表示第i个强连通分量中存在多少点
    vector<int>a[maxn];
    void tarjan(int u)//dfs(u)结束后 low[u]、pre[u]将会求出
    {
        dfn[u] = low[u] = ++dfs_clock;
        S.push(u);
        for (auto v : Map[u])//u->v
        {
            if (!dfn[v])//说明v还未被dfs
            {
                tarjan(v);//会自动求出low[v]、dfn[v]
                low[u] = min(low[u], low[v]);
            }
            else if (!sccno[v])//说明v正在dfs:只求出了dfn[v]
                low[u] = min(low[u], dfn[v]);
        }
        if (dfn[u] == low[u])//说明找到了一个强连通分量
        {
            scc_cnt++;
            for (;;)
            {
                int x = S.top(); S.pop();
                sccno[x] = scc_cnt;
                num[scc_cnt]++;
                a[scc_cnt].push_back(x);
                if (x == u)break;
            }
        }
    }
    
    void find_scc()
    {
        dfs_clock = scc_cnt = 0;
        memset(dfn, 0, sizeof(dfn));
        memset(sccno, 0, sizeof(sccno));
        for (int i = 1; i <= n; i++)
            if (!dfn[i])//dfn[i] == 0 说明还没走第i个点
                tarjan(i);
    }
    
    int chu[maxn];//chu[i]表示第i个强连通分量的出度
    int main()
    {
        cin >> n >> m;
        for (int i = 1; i <= m; i++)
        {
            int u, v;
            cin >> u >> v;
            Map[u].push_back(v);
        }
        find_scc();//找出所有的强连通分量
        for (int u = 1; u <= n; u++)//重建图
        {
            for (auto v : Map[u])
            {
                //u->v
                if (sccno[u] == sccno[v])//说明u,v属于同一个
                    continue;
                chu[sccno[u]]++;
            }
        }
        //缩点后 scc_cnt个点
        int number = 0, ans;//number表示出度为0的强连通分量的数量
        for (int i = 1; i <= scc_cnt; i++)
            if (chu[i] == 0)//出度为0
            {
                number++;
                ans = num[i];
            }
        if (number != 1)ans = 0;
        cout << ans << endl;
        return 0;
    }

    例题:HDU5934 Bomb 

    题面:

    题解:先根据题意构图,题意为A、B中心点的距离如果小于A的爆炸半径,认为A->B有一条有向边;如果小于B的爆炸半径,则认为B->A有一条有向边

    所以只要先存图,存完之后就是一个tarjan的裸题。

    代码:

    #include <iostream> 
    #include <stack>
    #include <vector>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    typedef long long ll;
    const int maxn = 1e4 + 10;
    vector<int>Map[maxn];
    vector<int>a[maxn];
    int n, m;
    int low[maxn];      //low[i]表示从i出发能够回到的最远的祖先
    int sccno[maxn];    //sccno[i]表示i属于第sccno[i]个强连通分量
    int dfn[maxn];      //DFS序就是DFS时某个点是第几个被访问的,用dfn[i]表示i的时间戳
    int dfs_clock;      //时间戳
    int scc_cnt;        //强连通分量的个数
    
    stack
    <int>S; int num[maxn];//num[i]表示第i个强连通分量中存在多少点 void tarjan(int u)//dfs(u)结束后 low[u]、pre[u]将会求出 { dfn[u] = low[u] = ++dfs_clock; S.push(u); for (auto v : Map[u])//u->v { if (!dfn[v])//说明v还未被dfs { tarjan(v);//会自动求出low[v]、dfn[v] low[u] = min(low[u], low[v]); } else if (!sccno[v])//说明v正在dfs:只求出了dfn[v] low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u])//说明找到了一个强连通分量 { scc_cnt++; for (;;) { int x = S.top(); S.pop(); sccno[x] = scc_cnt; num[scc_cnt]++; a[scc_cnt].push_back(x); if (x == u)break; } } } void find_scc() { dfs_clock = scc_cnt = 0; memset(dfn, 0, sizeof(dfn)); memset(low, 0, sizeof(low)); memset(sccno, 0, sizeof(sccno)); for (int i = 1; i <= n; i++) if (!dfn[i])//dfn[i] == 0 说明还没走第i个点 tarjan(i); } struct node { ll x, y, r, c; }p[maxn]; ll col(ll x1, ll y1, ll x2, ll y2) { ll dis = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); return dis; } ll cost_min[maxn];//记录第i个强连通分量的最小费用 int in[maxn];//记录重构图后的入度 void init(void) { memset(num, 0, sizeof(num)); memset(cost_min, 0x3f, sizeof(cost_min)); memset(in, 0, sizeof(in)); for (int i = 0; i < maxn; i++) Map[i].clear(), a[i].clear(); } int main() { int t; cin >> t; for (int num = 1; num <= t; num++) { init(); cin >> n; for (int i = 1; i <= n; i++) cin >> p[i].x >> p[i].y >> p[i].r >> p[i].c; for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n; j++) { if (col(p[i].x, p[i].y, p[j].x, p[j].y) <= p[i].r * p[i].r) Map[i].push_back(j); if (col(p[i].x, p[i].y, p[j].x, p[j].y) <= p[j].r * p[j].r) Map[j].push_back(i); } } find_scc();//找出所有的强连通分量 for (int i = 1; i <= n; i++) cost_min[sccno[i]] = min(cost_min[sccno[i]], p[i].c); //缩点 for (int u = 1; u <= n; u++) { for (auto v : Map[u]) { if (sccno[u] == sccno[v]) continue; in[sccno[v]]++; } } ll ans = 0; for (int i = 1; i <= scc_cnt; i++) { if (in[i] == 0) ans += cost_min[i]; } printf("Case #%d: %lld ", num, ans); } return 0; }
  • 相关阅读:
    CF 461B Appleman and Tree
    POJ 1821 Fence
    NOIP 2012 开车旅行
    CF 494B Obsessive String
    BZOJ2337 XOR和路径
    CF 24D Broken robot
    POJ 1952 BUY LOW, BUY LOWER
    SPOJ NAPTIME Naptime
    POJ 3585
    CF 453B Little Pony and Harmony Chest
  • 原文地址:https://www.cnblogs.com/ZJNU-huyh/p/13307574.html
Copyright © 2011-2022 走看看