SCC-Tarjan,缩点问题

发布时间:2023年12月18日

前言

图论中的缩点问题通常是指在有向图中,通过将强连通分量内的所有节点缩成一个节点,从而简化图的结构,这个过程称为缩点。这样做可以帮助我们分析和解决一些实际问题。

阅读本文前如果不了解Tarjan算法求解强连通分量,建议先看:SCC-Tarjan算法,强连通分量算法,从dfs到Tarjan详解-CSDN博客

阅读本文前如果不了解拓扑排序,建议先看:拓扑排序详解-CSDN博客


引例

一位老板刚成立一家公司,招揽了数名员工,各司其职,分工合作,共谋富贵。随着公司良性运转,又陆续招了一批批员工,随着员工数目增加,他们的工作内容,职位关系也都变得复杂,老板只觉管理力不从心。后来他发现那些工作内容相同的员工之间都需要进行业务交流,而工作内容不同的员工之中只有少数几个leader之间会进行业务来往,他灵机一动,将公司成员划分到了不同的部门之中,以后处理业务只需要下发到相关的各个部门中不需要具体的各个员工都找来一遍。随后公司管理压力骤减,工作效率大大提升,蒸蒸日上……

上面将成员之间复杂的业务关系简化为了各个部门之间的业务关系,将具有相同工作内容的员工整合为了一个部门,这就是我们中缩点的应用。

什么是缩点?

在有向图中,将每个强连通分量中的所有节点缩成一个节点,称为缩点(Vertex Contraction),两个不同强连通分量节点间若存在连边则将两个缩点间添加一条有向边,这样我们就将原本的有向有环图转化为了有向无环图

缩点的应用

缩点显然降低了图的规模,可以降低我们分析问题的复杂程度,但是对于缩点后的图的性质的维护,需要我们慎重考虑。

我们下面通过例题来感受缩点的精妙之处。

一、合并强连通子图为强连通图

题目描述

共有 n 所学校 (1 <= n <= 10000) 已知他们实现设计好的网络共 m 条线路,为了保证高速,网络是单向的。现在请你告诉他们至少选几所学校作为共享软件的母机,能使每所学校都可以用上。再告诉他们至少要添加几条线路能使任意一所学校作为母机都可以使别的学校使用上软件。

输入/输出格式

输入
第一行一个正整数 n。
接下来 n 行每行有若干个整数,用空格隔开。
第 i+1 行,每行输入若干个非零整数 x,表示从 i 到 x 有一条线路。以 0 作为结束标志。

输出
第一行一个整数,表示至少选几所学校作为共享软件的母机,能使每所学校都可以用上。
第二行一个整数,表示至少要添加几条线路能使任意一所学校作为母机都可以使别的学校使用上软件。
1 <= n <= 10000,1 <= 边数 <= 50000

原题链接

[P2812 校园网络【USACO]Network of Schools加强版】 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目详解

显然学校间的网络通信关系构成了一张有向图,其中有若干强连通分量,先忽略第一问,我们发现第二问就是问转化为强连通图的最少添加边数。这个问题下显然很庞大,我们不妨利用缩点,将原先的有向有环图转化为有向无环图,在进行研究。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

左上图为样例一的有向图表示,显然有三个强连通分量:{1,2,4},{3},{5}

右上图为缩点表示,得到了三个点{1,2,3}

那么我们回看问题,

**对于问题一:**我们观察缩点图,我们发现只需要两个母机,即1,3即可令所有学校可以用得上网络,那么如何得到一般情况下的解呢?

我们知道缩点图是一个有向无环图,换言之就是若干棵有向树组成的森林,那么每棵有向树的有一个根,根的数目就是母机的数目。而对于有向树的根,显然就是入度为0的点,则第一问的答案就是缩点图中入度为0点的数目。

**对于问题二:**问题二等价为将缩点图转化为强连通图,而对于强连通图每一个点入度出度都不为0,我们统计缩点图中入度为0的点sum1和出度为0的点sum2,那么需要添加的最小边数就是max(sum1,sum2)。注意可能输入样例给的就是强连通图,所以要特判。

AC代码如下:

#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <bitset>
using namespace std;
#define MOD 1000000007
#define N 10010
#define sc scanf
bitset<N> instk;
int dfn[N]{0}, low[N]{0}, st[N], head[N], scc[N], sz[N], cnt = 0, idx = 0, top = 0, tot = 0;
struct edge
{
    int v, nxt;
} edges[50010];
void addedge(int u, int v)
{
    edges[idx] = {v, head[u]};
    head[u] = idx++;
}
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;
    st[top++] = x, instk[x] = 1;
    for (int j = head[x]; ~j; j = edges[j].nxt)
    {
        int v = edges[j].v;
        if (!dfn[v])
        {
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }
        else if (instk[v])
        {
            low[x] = min(low[x], dfn[v]);
        }
    }
    if (low[x] == dfn[x])
    {
        int y;
        cnt++;
        do
        {
            y = st[--top];
            instk[y] = 0;
            scc[y] = cnt;
            sz[cnt]++;
        } while (x != y);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    memset(head, -1, sizeof(head));
    int n, x = 0, y = 0;
    cin >> n;
    for (int i = 1, a; i <= n; i++)
        while (cin >> a, a)
            addedge(i, a);

    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
    vector<int> in(cnt + 1), out(cnt + 1);
    for (int i = 1; i <= n; i++)
        for (int j = head[i]; ~j; j = edges[j].nxt)
            if (scc[i] != scc[edges[j].v])
            {
                in[scc[edges[j].v]]++, out[scc[i]]++;
            }
    for (int i = 1; i <= cnt; i++)
        x += !in[i], y += !out[i];
    cout << x << '\n';
    cout << (cnt != 1 ? max(x, y) : 0);
    return 0;
}

二、集合间偏序关系

题目描述

每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A 喜欢 B,B 喜欢 C,那么 A 也喜欢 C。牛栏里共有 N 头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。

输入/输出格式

输入

第一行:两个用空格分开的整数:N 和 M。
接下来 M 行:每行两个用空格分开的整数:A 和 B,表示 A 喜欢 B。

输出
一行单独一个整数,表示明星奶牛的数量。

原题链接

[P2341 USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目详解

显然明星牛就是所有牛到其自身都有路径可达的牛,那么如果明星牛不唯一的话,这些明星牛彼此之间互相可达自然在一个强连通分量内,所以我们还是在缩点图上研究问题。

这样我们就又得到了个有向无环图,那么对于明星牛集合的缩点,其必须满足出度为0,否则就不配被称为明星牛(bushi

其实如果学过离散数学或者抽象代数的话,明星牛就是偏序关系哈斯图中的最大上界,没学过也没关系,只要明白对于明星牛缩点,必然满足所有缩点到其都有路径,也就是说出度为0的缩点只有明星牛缩点一个,否则会出现有小黑子牛不喜欢明星牛(bushi

AC代码如下:

#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <bitset>
using namespace std;
#define MOD 1000000007
#define N 10010
#define sc scanf
bitset<N> instk;
int dfn[N]{0}, low[N]{0}, st[N], head[N], scc[N], sz[N], cnt = 0, idx = 0, top = 0, tot = 0;
struct edge
{
    int v, nxt;
} edges[50010];
void addedge(int u, int v)
{
    edges[idx] = {v, head[u]};
    head[u] = idx++;
}
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;
    st[top++] = x, instk[x] = 1;
    for (int j = head[x]; ~j; j = edges[j].nxt)
    {
        int v = edges[j].v;
        if (!dfn[v])
        {
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }
        else if (instk[v])
        {
            low[x] = min(low[x], dfn[v]);
        }
    }
    if (low[x] == dfn[x])
    {
        int y;
        cnt++;
        do
        {
            y = st[--top];
            instk[y] = 0;
            scc[y] = cnt;
            sz[cnt]++;
        } while (x != y);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    memset(head, -1, sizeof(head));
    int n, m, x = 0, y = 0;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> x >> y;
        addedge(x, y);
    }

    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
    vector<int> out(cnt + 1);
    for (int i = 1; i <= n; i++)
        for (int j = head[i]; ~j; j = edges[j].nxt)
            if (scc[i] != scc[edges[j].v])
                out[scc[i]]++;
    int ans = 0, zero = 0;
    for (int i = 1; i <= cnt; i++)
        if (!out[i])
            ans = sz[i], zero++;
    cout << (zero <= 1 ? ans : 0);
    return 0;
}

三、最大点权和路径

题目描述

给定一个 n 个点 m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

输入/输出格式

输入
第一行两个正整数 n,m
第二行 n 个整数,其中第 i 个数 ai 表示点 i 的点权。
第三至 2m + 2 行,每行两个整数 u , v ,表示一条 u→v 的有向边。

输出
共一行,最大的点权之和。

原题链接

P3387 【模板】缩点 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目详解

对于求点权和问题,很多时候都可以转化为生成树问题,但是对于本题他要求的是最大点权路径,如果是有向无环图我们可以通过拓扑+动态规划来进行求解,但是本题要求是**“允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次”**,那么这样的话我们直接在原图上求解CPU会干烧的(精神上

那么怎么办呢?自然是缩点大法,我们缩点的权值就是集合内权值之和,那么我们就转化为了在缩点图这样一个有向无环图上求最大点权路径,那么我们用拓扑+动态规划进行求解即可。

但是我们注意到,我们的SCC是用Tarjan来求解的,而Tarjan是利用时间戳求解SCC即先访问的SCC其SCC编号反而大,那么我们按照编号从大到小访问缩点自然就是拓扑序了

以下图为例,我们发现节点编号从大到小来看刚好为拓扑序

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

AC代码如下:

#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <cmath>
#include <functional>
#include <climits>
#include <bitset>

using namespace std;
#define MOD 1000000007
#define N 10010
#define sc scanf
bitset<N> instk;
int dfn[N]{0}, low[N]{0}, dp[N]{0}, st[N], head[N], Head[N], scc[N], sz[N], cost[N], cnt = 0, idx = 0, Idx = 0, top = 0, tot = 0;
struct edge
{
    int v, nxt;
} edges[50010], Edges[50010];
void addedge(int u, int v)
{
    edges[idx] = {v, head[u]};
    head[u] = idx++;
}
void Addedge(int u, int v)
{
    Edges[Idx] = {v, Head[u]};
    Head[u] = Idx++;
}
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;
    st[top++] = x, instk[x] = 1;
    for (int j = head[x]; ~j; j = edges[j].nxt)
    {
        int v = edges[j].v;
        if (!dfn[v])
        {
            tarjan(v);
            low[x] = min(low[x], low[v]);
        }
        else if (instk[v])
        {
            low[x] = min(low[x], dfn[v]);
        }
    }
    if (low[x] == dfn[x])
    {
        int y;
        cnt++;
        do
        {
            y = st[--top];
            instk[y] = 0;
            scc[y] = cnt;
            sz[cnt] += cost[y];
        } while (x != y);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    memset(head, -1, sizeof(head));
    memset(Head, -1, sizeof(Head));

    int n, m, x = 0, y = 0;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> cost[i];
    for (int i = 0; i < m; i++)
    {
        cin >> x >> y;
        addedge(x, y);
    }

    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

    for (int i = 1; i <= n; i++)
        for (int j = head[i]; ~j; j = edges[j].nxt)
            if (scc[i] != scc[edges[j].v])
                Addedge(scc[i], scc[edges[j].v]);
    int ans = 0;
    for (int i = cnt; i; i--)
    {
        if (!dp[i])
            ans = max(ans, dp[i] = sz[i]);
        for (int j = Head[i]; ~j; j = Edges[j].nxt)
            ans = max(ans, dp[Edges[j].v] = max(dp[Edges[j].v], dp[i] + sz[Edges[j].v]));
    }
    cout << ans;
    return 0;
}

其他OJ练习

P2002 消息扩散 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P1262 间谍网络 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

文章来源:https://blog.csdn.net/EQUINOX1/article/details/135070182
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。