有向图的强连通分量(一)

有向图的强连通分量

概念

对于一个有向图:
连通分量:对于分量里的任意两点u, v,必然可以从u走到v,且从v走到u。连通分量中任意两点双向互通。
强连通分量(SCC):极大连通分量。在此图中,对于一个连通分量,图中其他任意点加入均不能构成连通分量,则此连通分量为极大连通分量,即强连通分量。

强连通分量可以将任意一个有向图通过缩点,转化为有向无环图(DAG),即拓扑图。易得有向无环图中单独的一个点是强连通分量。

缩点:将所有连通分量缩为一个点。红圈为联通分量。缩点之后将图转化为树。

逻辑上来讲,单个的点也是强连通分量,那么强连通分量就可以在某些情况下看作一个点。
在这里插入图片描述
求强连通分量:dfs序搜索。
搜索过程中的边可分为四类:①树枝边(x, y),x是y的父节点②前向边(x, y),x是y的祖先节点。易得树枝边是特殊的前向边
③后向边(x, y),x是y的祖先节点,但该边由y指向x④横叉边:该边连向其他搜索枝条的边,从定义上讲横叉边可能向左或向右联通,但事实上确定dfs方向后,横叉边只会出现在连向已经搜索过的枝条的边,首次连向另一侧的枝条的边会被定义为树枝边。

一个点是否在强连通分量中:
①该点存在后向边指向祖先节点
②该点B存在横叉边,横叉边连向某点A,此点A存在后向边指向祖先节点。

Tarjan算法求SCC

概念:

​ 时间戳:timestamp。一句dfs序列顺序为图中各个点定义时间戳,即遍历先后顺序。
​ 数组dfn保存遍历到点i的时间dfn[i],即遍历次序。
​ 数组low:low[u]指从u开始所能遍历到的时间戳最小的点的时间戳。即能遍历到的点中最早被遍历的。包括自己。

​ 如果u是其所在的强连通分量的最高点,即low[u] == dfn[u]。
​ 如果low[u] == dfn[u],则点u是所在强连通分量的最高点。

​ 时间复杂度为O(n + m)

模板:

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]) //
    {
        int y;
        ++scc_cnt;
        do
        {
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
        } while (y != u);
    }
}

缩点加边:

伪代码:
for (int i = 1; i <= n; i++)
{
    for (i的所有邻点)
    {
        if (i, j不在同一scc中)
        {
            加一条新边id[i] -> id[j];
        }
    }
}

易得在tarjan算法、缩点完成后,由于根据时间戳保存了遍历顺序,得到DAG本身就是有序的拓扑图。即连通分量编号递减的顺序就是拓扑序。DAG中的点可能是单个的点,也可能是强连通分量。可以通过缩点完成后的scc_cnt个点的出度入度解决问题。

出度入度计算:
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)
        {
            dout[a]++;
            din[b]++;
        }
    }
}

关于出入度的结论:若求为一个图加多少条边可使整个图为强连通分量,则首先求出当前强连通分量,缩点,易得当前图为DAG。遍历当前图中点,记录出度为0以及入度为0的点的个数,即起点、终点,设为p. q,则需要加max(p, q)条边。

例题

Acwing1174
在这里插入图片描述
代码:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = 50010;

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, sizes[N];
int dout[N], din[N];

void add(int a, int b)  // 添加一条边a->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] = 1;

    for (int i = h[u]; i != -1; 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 (low[u] == dfn[u])
    {
        ++scc_cnt;
        int y;
        do
        {
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            sizes[scc_cnt]++;
        } while (y != u);
    }

}


int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    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)
            {
                dout[a]++;

            }
        }
    }

    int zeros = 0, sum = 0;
    for (int i = 1; i <= scc_cnt; i++)
    {
        if (!dout[i])
        {
            zeros++;
            sum += sizes[i];
            if (zeros > 1)
            {
                sum = 0;
                break;
            }
        }
    }
    cout << sum;
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值