强连通分量(Tarjan算法)和缩点

本文深入探讨了有向图的强连通分量概念,通过Tarjan算法详细解释如何在线性时间内找到这些分量。在深度优先遍历过程中,利用回溯值和栈来维护强连通分量,并介绍了缩点操作,即将环视为单个节点以简化后续的图论计算。此外,提供了邻接矩阵和邻接表两种数据结构下的算法实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

强连通分量(Tarjan算法)和缩点

一些定义

给定一张有向图,对于图中任意两个节点 x x x y y y ,存在从 x x x y y y 的路径,也存在从 y y y x x x 的路径,则称该有向图为强连通图

有向图的强连通子图被称为强连通分量SCC

显然,环一定是强连通图。因为如果在有向图中存在 x x x y y y 的路径,且存在 y y y x x x 的路径,那么 x , y x,y x,y 一定在同一个环中。

对于一个有向图,如果从 r o o t root root 可以到达图中所有的点,则称其为“流图”,而 r o o t root root 为流图的源点。

r o o t root root 为原点对流图深度优先遍历,每个点只访问一次,在过程中,所有发生递归的边 ( x , y ) (x, y) (x,y) 构成的一棵树叫做这个流图的搜索树

在深度优先遍历时,对每个访问到的节点分别进行整数 1... n 1...n 1...n 标记,该标记被称为时间戳,记为 d f n [ x ] dfn[x] dfn[x]

流图中的每条有向边 ( x , y ) (x, y) (x,y) 必然是以下四种中的一种:

  1. 前向边,指搜索树中 x x x y y y 的祖先节点
  2. 后向边,指搜索树中 y y y x x x 的祖先节点
  3. 树枝边,指搜索树里的边,满足 x x x y y y 的父节点。
  4. 其他边(好像也叫横叉边),指除了上面三种情况的边。且一定满足 d f n [ y ] < d f n [ x ] dfn[y]<dfn[x] dfn[y]<dfn[x]

Tarjan算法之强连通分量

Tarjan 算法基于有向图的深度优先遍历,能够在线性时间中求出一张有向图的各个强连通分量。

其核心思想就是考虑两点之间是否存在路径可以实现往返。

我们在后文中,都会结合搜索树(本身就是深度优先遍历的产物)来考虑,这样就可以在深度优先遍历的同时完成我们的目标。

对于各种边的分析:

对于流图,前向边作用不大,因为当前搜索树中一定存在 x x x y y y 的路径。

后向边就很重要了,因为它一定可以和搜索树中 x x x y y y 的路径组成环。

横叉边需要判断一下,如果这条横叉边能到达搜索树上 x x x 的祖先(显然, x x x 的祖先一定可以到达 x x x)。记这个祖先为 z z z,则这条横叉边一定能和它到 z z z 的路径, z z z x x x 的路径组成环。

强连通分量的维护和栈的引入:

为了找到横叉边与后向边组成的环,我们考虑在深度优先遍历的同时维护一个栈。

当遍历到 i i i 节点时,栈里一定有以下一些点:

  1. 搜索树上 i i i 的所有祖先集合 j j j。若此时存在后向边 ( i , j ) (i, j) (i,j),则 ( i , j ) (i, j) (i,j) 一定与 j j j i i i 的路径形成环。(后向边组成的环)
  2. 已经访问过的点 k k k,且满足从 k k k 出发一定能找到到 j j j 的路径。此时, i i i k k k 的路径, k k k j j j 的路径, j j j i i i 的路径一定会形成环。(横叉边和后向边组成的环)

于是我们引入回溯值的概念。回溯值 l o w [ x ] low[x] low[x] 表示以下节点的最小时间戳:

  1. 该点在栈中。
  2. 存在一条从流图的搜索树中以 x x x 为根的子树为起点出发的有向边,以该点为终点。(也就是以它为起点能继续往下遍历到的点)

如果当前的 l o w [ x ] low[x] low[x] 表示的最小时间戳代表的点集全是2类点,则易得 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x] 时强连通分量存在,且 x x x 是此强连通分量的根(整个强连通分量中时间戳最小的节点)。

如果表示的点集存在1类点。则当前点一定属于强连通分量,且该强连通分量的根为整个强连通分量中时间戳最小的节点。

当我们判断了存在以当前点为根的强连通分量后,从栈中不断取出点,直到取出的点与当前点相等,我们就得到了整个强连通分量的信息。

整理更新回溯值的方法:

如果当前点第一次被访问,入栈,且 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x]

遍历从 x x x为起点的每一条边 ( x , y ) (x,y) (x,y) 。若 y y y 被访问过,且 y y y 在栈中,那么 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])。若y没被访问过,先递归访问y,在回溯之后更新 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])


具体实现:

1 邻接矩阵建图

#include<bits/stdc++.h>
using namespace std;
#define MAXN 200005
#define X 114514
int n;//一共有n个点
//强连通分量相关定义

int dfn[MAXN],low[MAXN];//时间戳以及回溯值(最小时间戳)
struct node
{
    int mx;//集团中点权的最大值
    int sum;//缩点后的边权和
    //其他信息根据具体题目具体设置
}sccc[MAXN];//储存最后求出的各个强连通分量的信息
int key[MAXN];//key[i]表示i在编号为key[i]的强连通分量中
int num=0;//时间戳
int scc=0;//强连通分量个数
stack <int> s;
int visit[MAXN];//记录是否在栈中

//

vector <int> original_mp[MAXN];//原始图

void tarjan(int x)
{
    low[x]=dfn[x]=++num;
    s.push(x);visit[x]=1;
    for(auto y:original_mp[x])//遍历原始图
    {
        if(dfn[y]==0)
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(visit[y]==1)
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(dfn[x]==low[x])//维护这个强连通分量
    {
        scc++;
        int now=-1;
        while(x!=now)
        {
            now=s.top();s.pop();
            visit[now]=0;
            key[now]=scc;
            sccc[scc].mx=X;
            sccc[scc].sum=X;
            //...其他信息根据具体题目具体分析,这里只是举例维护的方法
        }
    }
}

void init()
{
    scc=0;num=0;
    for(int i=1;i<=n;i++)
    {
        key[i]=i;
        dfn[i]=0;low[i]=0;visit[i]=0;
        //sccc[i].clear(); 要不要清除,怎么清除,根据具体题目来
    }
}

void solve()
{
    init();
    for(int i=1;i<=n;i++)
    {
        if(dfn[i]==0)//如果当前点没被遍历过,跑一遍Tarjan
            tarjan(i);
    }
}


2 邻接表建图

#include<bits/stdc++.h>
using namespace std;
#define MAXN 200005//点数
#define MAXM 500005//边数
#define X 114514
int n,m;//点和边的数量
//邻接表相关定义
struct Edge
{
    int to,next;
}edge[MAXM];
int head[MAXN],tot;
//

//强连通分量相关定义
int low[MAXN],dfn[MAXN],key[MAXN],visit[MAXN];
stack <int> s;
int scc=0;//强连通分量个数
int num=0;//时间戳个数
struct node
{
    int mx;//集团中点权的最大值
    int sum;//缩点后的边权和
    //其他信息根据具体题目具体设置
}sccc[MAXN];//储存最后求出的各个强连通分量的信息

int siz[MAXN];//各个强连通分量包含点的个数,不一定需要
//

void init()
{
    tot=0;
    for(int i=0;i<=m;i++) head[i]=-1;
    scc=0;num=0;
    for(int i=1;i<=n;i++)
    {
        key[i]=i;
        dfn[i]=0;low[i]=0;visit[i]=0;
        //sccc[i].clear(); 要不要清除,怎么清除根据具体题目来
    }
}

void tarjan(int x)
{
    low[x]=dfn[x]=++num;//更新时间戳
    s.push(x);visit[x]=1;//入栈
    for(int i=head[x];i!=-1;i=edge[i].next)//遍历原始图
    {
        int y=edge[i].to;
        if(dfn[y]==0)
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(visit[y]==1)
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(low[x]==dfn[x])//维护当前强连通分量
    {
        scc++;
        int now=-1;
        while(now!=x)
        {
            now=s.top();s.pop();
            visit[now]=0;//出栈标记
            //维护当前强连通分量内节点信息
            key[now]=scc;
            sccc[scc].mx=X;
            sccc[scc].sum=X;
        }
    }
}

void solve()
{
    init();
    for(int i=1;i<=n;i++)
    {
        if(dfn[i]==0)
            tarjan(i);
    }
}

缩点

缩点其实就是指的把环看成一个点来进行后面的图论算法。而把环看成的这个点的点权在题目中会具体说明。

比较常见的缩点后的点权是整个环路中所有点的点权和。

如下图:

1 -> 2 -> 4 -> 5 -> 2 -> 3

显然上图存在环路,在经过缩点后,我们可以将它变成这样:

1 -> 2(val[2]+val[4]+val[5]) -> 3

具体实现:

#include<bits/stdc++.h>
using namespace std;
#define MAXN 200005//点
int n;//点
int key[MAXN];//之前定义的,表示这个点位于哪个强连通分量
vector <int> original_mp[MAXN];//原图
vector <int> mp[MAXN];//缩点后的图

void solve()
{
    for(int i=1;i<=n;i++)
    {
        for(auto j:original_mp[i])
        {
            if(key[i]==key[j]) continue;
            //如果这条边的两端同属于一个强连通分量,放弃连边
            mp[key[i]].emplace_back(key[j]);
            //将两个点对应的强连通分量的编号相连,加入新图。
            //显然,单个点也属于一个强连通分量
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值