图论学习-有向图强连通分量

文章目录

  • 有向图强连通分量
    • 1.定义:
    • 2.基本术语与概念
      • 2.1 边的概念
      • 2.2 缩点
      • 2.3 时间戳
    • 3. tarjan求强连通分量(SCC)
      • 3.1 原理
      • 3.2 步骤
      • 3.3 模板
        • 3.3.1 tarjan求强连通分量的过程
        • 3.3.2 缩点的过程
    • 4.例题
      • 题目1:P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
      • 题目2:P2746 [USACO5.3]校园网Network of Schools
    • 5.参考资料

有向图强连通分量

1.定义:

给定一张有向图,若对于图中任意两个节点x,y,既存在从x到y的路径,也存在y到x的路径,则该有向图是强连通图。
有向图的极大强连通子图被称为:强连通分量,简称SCC(Strongly connected component)
Tarjan 算法基于dfs,可以在线性时间内求出一张有向图的所有连通分量。

2.基本术语与概念

2.1 边的概念

1.树枝边:搜索树中给定两个点x,y,x是y的父亲节点,那么这条边就是树枝边

2.前向边:搜索树中给定两个点x,y,x是y的祖先节点,那么这条边就是前向边

3.后向边:搜索树中给定两个点x,y,y是x的祖先节点,那么这条边就是后向边

4.横叉边:搜索树中给定两个点x,y,不属于以上三种的边,就叫横叉边,且y在搜索树中的编号一定小于x在搜索树中的编号。即dfn[y]

举个例子:

dfn数组存储的即是各个深度优先遍历过程中,每个节点第一次被访问的时间顺序,读者不妨对该图做一次深度优先遍历加以验证。
例如:1,2即是树枝边,注意:树枝边一定也是前向边
1,8即是前向边,4,6即是后向边,8,7是横叉边
图论学习-有向图强连通分量_第1张图片
图论学习-有向图强连通分量_第2张图片

2.2 缩点

将所有连通分量缩成一个点。
图论学习-有向图强连通分量_第3张图片

2.3 时间戳

在搜索的时候,按照深度优先遍历顺序给每个点一个编号,即时间戳。
dfn[u]表示遍历到u的时间戳。

3. tarjan求强连通分量(SCC)

3.1 原理

  1. 把边分成四大类:树枝边,前向边,后向边,横叉边
  2. 取一个点x,判断它是否在强连通分量中
    情况1:存在一条后向边使得它可以走到某个祖先节点
    情况2:存在一条横叉边,它先通过横叉边,然后横叉边所在的点可以走到某个祖先节点
    以上两种情况,点x和它的祖先节点之间所有节点都可以互相到达,满足强连通分量的定义。

3.2 步骤

  1. 给每个点定义两个时间戳:
    dfn[u]表示遍历到u的时间戳,即dfs的搜索顺序
    low[u]表示从u开始走,所能遍历到的最小时间戳
  2. u是其所在强连通分量的最高点,等价于dfn[u]==low[u]

3.3 模板

3.3.1 tarjan求强连通分量的过程

void tarjan(int u)
{
   dfn[u] = low[u] = ++timestamp;//时间戳
   stk[++top] = u,in_stk[u] = true;//把当前点放到栈里面去,随后标记u已经入栈
   for(int i = h[u];~i;i = ne[i])
   {
       int j = e[i];
       if(!dfn[j])
       {
           tarjan(j);//先看一下u能到的点
           //这里做完tarjan(j)以后,low[j]已经被更新了,由定义可知,low[j]保存的就是从j出发能到达的时间戳最小的点
           low[u] = min(low[u],low[j]);//更新low[u]
       }
       else if(in_stk[j])
       {
           //可能通过其他边能搜到j,但不是当前分支搜到的,读者这里可以好好考虑考虑
           //stk不一定只存了当前分支的点,还可能存有前面其他分支的点,比如横叉边所到达的点
           //stk存的是当前还没有遍历完的强连通分量的所有点
           low[u] = min(low[u],dfn[j]);
       }
   }
   if(dfn[u]==low[u])
   {
       //u必然是u所在的强连通分量的最高点(由定义)
       int y;
       ++scc_cnt;
       do{
           y = stk[top--];//把栈顶弹出
           in_stk[y] = false;//标记它没有在栈里面
           id[y] = scc_cnt;//标记y这个点属于scc_cnt这个强连通分量
       }while(y!=u);
   }
}
// y总原话:《最好背过》 :)

3.3.2 缩点的过程

for(int i = 1;i<=n;i++)
{
   for(i的所有邻点j)
   {
       if(i和j不在同一个强连通分量中)
       {
           把i所在的强连通分量向j所在的强连通分量加一条边;
       }
   }
}
//缩完点就是DAG(有向无环图)

注:缩完点以后,连通分量编号递减的顺序一定是拓扑序

给出注的证明:
对于一个图做一遍深度优先遍历,把每个点加入序列seq中
显然有seq的逆序即是拓扑序。

for(int i = h[u];~i;i = ne[i])
{
    int j = e[i];
    if(!st[j])
    {
        seq<-j;//把j存入seq序列中
        dfs(j);
    }
}
//读者不妨验证一下

最后,观察tarjan求有向图强连通分量的过程实际上就是对图做深度优先遍历的过程,所以最后按连通分量编号递减的顺序输出一定是拓扑序

4.例题

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

一个思路是建反图,对每个奶牛做一遍dfs,统计完美奶牛个数,时间复杂度O(nm),显然超时。
再观察:
题意知当是有向无环图时,只要有两个点出度为0,那么完美奶牛个数为0,如果有一个出度为0,那么完美奶牛个数为1,
于是可以tarjan缩点,如果只有一个出度为0的强连通分量,那么答案就是该强连通分量的size。

#include
using namespace std;
const int N = 1e4+10,M = 5e4+10;
int n,m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;  // 时间戳
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;  // 每个点所属分量编号
int dout[N],sz[N];//出度数组

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{//缩点
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u,in_stk[u] = true;
    for(int i = h[u];~i;i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u],low[j]);
        }
        else if(in_stk[j])
        {
            low[u] = min(low[u],dfn[j]);
        }
    }
    if(dfn[u]==low[u])
    {
        ++scc_cnt;
        int y;
        do{
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sz[scc_cnt]++;
        }while(y!=u);
    }
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n>>m;    
    while (m -- ){
        int a,b;
        cin>>a>>b;
        add(a, b);
    }
    for(int i = 1;i<=n;i++)
    {
        if(!dfn[i])
        {
            tarjan(i);
        }
    }
    for(int i = 1;i<=n;i++)
    {
        for(int j = h[i];~j;j = ne[j])
        {
            int k = e[j];
            int a = id[i],b = id[k];
            if(a!=b)
            {
                //说明i和j不在同一个连通分量中
                dout[a]++;//这里没有实际把边加出来,因为不在同一个连通分量中,所以要把a的出度+1,等价于加了一条边
            }
        }
    }
    int sum = 0,zeros = 0;//zeros记录出度为0的连通分量的个数
    for(int i = 1;i<=scc_cnt;i++)
    {
        if(!dout[i])
        {
            zeros++;
            sum+=sz[i];
            if(zeros>1)
            {
                cout<<0;
                return 0;
            }
        }
    }
    cout<<sum;
    return 0;
}

题目2:P2746 [USACO5.3]校园网Network of Schools

第一个问类似题目1,第二个问需要找小性质:如何让有向无环图变成强连通图添加的边的数量最少?
设 a 为 入 度 为 0 的 点 的 个 数 , b 为 出 度 为 0 的 点 的 个 数 那 么 易 有 a n s = m a x ( a , b ) 设a为入度为0的点的个数,b为出度为0的点的个数\\ 那么易有ans = max(a,b) a0b0ans=max(a,b)
题解来自《算法竞赛进阶指南》:
图论学习-有向图强连通分量_第4张图片
AC code:

#include
using namespace std;
const int N = 1e4+10,M = 5e4+10;
int n,m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;  // 时间戳
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;  // 每个点所属分量编号
int dout[N],sz[N],din[N];//出度数组

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{//缩点
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u,in_stk[u] = true;
    for(int i = h[u];~i;i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u],low[j]);
        }
        else if(in_stk[j])
        {
            low[u] = min(low[u],dfn[j]);
        }
    }
    if(dfn[u]==low[u])
    {
        ++scc_cnt;
        int y;
        do{
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sz[scc_cnt]++;
        }while(y!=u);
    }
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n;
    for(int i = 1;i<=n;i++)
    {
        int x;
        while(cin>>x,x)
        {
            add(i,x);
        }
    }
    for(int i = 1;i<=n;i++)
    {
        if(!dfn[i])
        {
            tarjan(i);
        }
    }
    for(int i = 1;i<=n;i++)
    {
        for(int j = h[i];~j;j = ne[j])
        {
            int k = e[j];
            int a = id[i],b = id[k];
            if(a!=b)
            {
                //说明i和j不在同一个连通分量中
                dout[a]++;//这里没有实际把边加出来,因为不在同一个连通分量中,所以要把a的出度+1,等价于加了一条边
                din[b]++;
            }
        }
    }
    int sum1 = 0,sum2 = 0,cnt1 = 0,cnt2 = 0;
    //cnt1记录出度为0的连通分量的个数
    //cnt2记录入度为0的连通分量的个数
    for(int i = 1;i<=scc_cnt;i++)
    {
        if(!dout[i])
        {
            cnt1++;
        }
        if(!din[i])
        {
            cnt2++;
        }
    }
    if(scc_cnt!=1)
        cout<<cnt2<<endl<<max(cnt1,cnt2);
    else
        cout<<cnt2<<endl<<"0";
    return 0;
}

陆续更新~

5.参考资料

  1. acwing算法提高课图论
  2. 《算法竞赛进阶指南》

你可能感兴趣的:(#,tarjan算法与图的连通性,图论,算法,深度优先)