强连通分量入门——Trajan算法

今天学习了强连通分量。
【参考博客】
如果觉得我讲的有些地方难以理解或者存在问题(欢迎留言),可以看一下我借鉴的一些大佬的博客:
传送门1 传送门2
【知识储备】
首先我们需要对几个定义有一些概念:
强连通:有向图中两个点可以相互到达
强连通图:有向图中任意两个点都是强连通的
强连通分量:一个有向图的一个子图中是强连通图的最大的子图就称这个有向图为强连通分量

通俗理解的话,强连通分量里面的任何两个点都可以相互到达,也就是说至少存在一条路径可以访问到所有的点再回到起点(可以经过重复的点),就好像一个环,因此我们用DFS配合专门的算法来解决求连通分量的问题。

【算法介绍】
我们一般用Trajan算法解决相关问题。
正如上面的简单分析,所谓的强连通分量,就是我们想要找一条可以回到起点的经过尽可能多点的路径,那如何判断我们是回到起点(或者已经访问过的点)呢?我们就需要用数组进行标记每个点的状态来方便我们进行判断。
这里引入两个关键的数组:
D F N [ M A X N ] DFN[ MAXN ] DFN[MAXN] 用来标记DFS访问到该点时的次序/时间
L O W [ M A X N ] LOW[MAXN] LOW[MAXN] 用来储存子树(从这一点可以访问到的点)中访问时间最早的,初始化为DFN,也就是自身的访问时间。如果访问到了之前的点,就会变成前面的点的时间戳。

这样对于之前的点来说就形成了一条从自身出发又回到自身的路径,也就是一个强连通分量。

【算法实现】

对于每个点我们进行深搜,对每个点打上时间戳(给DFN和LOW赋值)

然后对每一个没有访问过的点直接进行 访问,并且将LOW的值改为所有后面访问点中最小的。
如果遇到一个访问过的点

如果他不在栈中,就说明他和这个强连通分量没有任何关系(在其他地方已经访问结束,无法到这个点,无法形成回路,否则这个点肯定在栈中)。这里着重需要理解栈中保存的是已经访问过的点中可以访问到这个点的点(其他无关的点都已经弹出)

如果这个已经访问的点在栈中(就比较开心,说明形成环了),如果这个点的最小的时间戳小于他就保存一下LOW,然后这个值就会回溯回去(有可能访问的这个点在另一个小的强连通分量中,所以我觉得应该比较LOW,但是我看其他人的博客都是比较DFN,仔细想了一下觉得也可以,保存DFN的话也可以,但是我还是觉得比较LOW的话LOW的值就可以代表是否存在在同一个强连通分量中,更加优雅一些 。emm,如果觉得不能理解可以先往下看,不要在在意这些细节 )。

最后DFS结束以后再回来看LOW的值是否改变,如果没有改变说明这后面的所有点构成一个强连通分量,然后全部弹出(和他没有关系的早早弹出去了,所以不用担心后面的没有关系的点怎么办)

如果对一个点进行DFS改变LOW的值后LOW的值还没有改变,仍然等于DFN,说明从这一个点出发是无法回到更早的点的,最早也就是自身,也就是说,他一定不是之前点的连通分量里面的(否则通过它肯定能够访问到之前的点,而之前的点的LOW都比较小),所以这个LOW没有改变的点就是一个连通分量的根节点(比较惨的话就只有他一个节点,但也有可能他的子节点会访问到他形成连通分量,但无论如何他都是根节点),我们不妨用一个栈保存访问的顺序,那么他后面访问的点肯定都是他的连通分量中的点,全部弹出即可。(如果不是的话,后面的点肯定自成强连通分量,那么肯定更早弹出了)。 可能稍微有些懵,先有个概念然后再看代码(注意是递归处理的,也就是访问到后面处理完了再返回来处理前面)。

看着代码理解一下吧,觉得哪里不能理解可以再看看上面的分析

void Trajan(int x)
{
    int v,tmp;

    DFN[x]=LOW[x]=++idx;	//赋给时间戳
    stack[++top]=x; 
    vis[x]=true;
    for(int i=head[x];i;i=Edge[i].next)
    {
        v=Edge[i].v;
        if(!DFN[v])
        {
            Trajan(v);
            if(LOW[v]<LOW[x]) LOW[x]=LOW[v];
        }
        else if(vis[v] && LOW[v]<LOW[x])
        {
            LOW[x]=LOW[v];
        }
    }
    if(DFN[x]==LOW[x])
    {
        cnt++;
        do
        {
            tmp=stack[top--];
            vis[tmp]=false;
            color[tmp]=cnt;
        }while (tmp!=x);
    }
}

【样例题目】
Popular Cows

Every cow's dream is to become the most popular cow in the herd. In a herd of N (1 <= N <= 10,000) cows, you are given up to M (1 <= M <= 50,000) ordered pairs of the form (A, B) that tell you that cow A thinks that cow B is popular. Since popularity is transitive, if A thinks B is popular and B thinks C is popular, then A will also think that C is
popular, even if this is not explicitly specified by an ordered pair in the input. Your task is to compute the number of cows that are considered popular by every other cow.

Input

* Line 1: Two space-separated integers, N and M

* Lines 2..1+M: Two space-separated numbers A and B, meaning that A thinks B is popular.

Output

*Line 1: A single integer that is the number of cows who are considered popular by every other cow.

Sample Input

3 3
1 2
2 1
2 3

Sample Output

1

题目大意:

有一群牛,他们相互崇拜,找出所有牛都崇拜的牛有多少

输入:牛的个数n 崇拜的关系m,然后m行每行A,B,表示A崇拜B

输出:被所有牛崇拜的牛的个数

【样例分析】
将崇拜的关系变成一个有向图,处于一个强连通分量里面的牛肯定是相互崇拜的,我们将一个强连通分量里面的所有牛看成一个点(染色),不同点重新形成一个图,这个图里面唯一的出度为0的点中牛的个数就是答案。
因为出度为0的点肯定是被其他点里的牛崇拜的,如果出度为0的点大于1的话两个出度为0的点里面的牛是没有办法崇拜的,所以肯定不能被所有的牛崇拜,所以如果出度为0的点不止一个答案就是0,否则就是出度为0 的点里面牛的个数(出度为0的点肯定是大于等于1的,如果没有出度为0 的点那么他们形成了一个环,而我们刚才说这是不同强连通分量,如果只有一个连通分量就只有一个点,也符合一个出度为0的点)

【AC代码】

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<climits>
#include<queue>
#include<vector>
#include<set>
#include<map>
using namespace std;

typedef long long ll;
const int MAXN=1e4+5;
const int MAXM=5e4+5;
struct node
{
    int v,next;
}Edge[MAXM];
int head[MAXN],tot;
int DFN[MAXN],LOW[MAXN];
int color[MAXN],cnt;
bool vis[MAXN];
int idx;
int stack[MAXN],top;
int OutDegree[MAXN];
int n,m;

void init()
{
    memset(head,0,sizeof(head)); tot=0;
    idx=0; memset(vis,0,sizeof(vis));
    memset(DFN,0,sizeof(DFN));
    memset(color,0,sizeof(color));
    cnt=0; top=0;
    memset(OutDegree,0,sizeof(OutDegree));
}

void read()
{
    int u,v;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&u,&v);
        tot++;
        Edge[tot].v=v; Edge[tot].next=head[u];
        head[u]=tot;
    }
}

void Trajan(int x)
{
    int v,tmp;

    DFN[x]=LOW[x]=++idx;
    stack[++top]=x; vis[x]=true;
    for(int i=head[x];i;i=Edge[i].next)
    {
        v=Edge[i].v;
        if(!DFN[v])
        {
            Trajan(v);
            if(LOW[v]<LOW[x]) LOW[x]=LOW[v];
        }
        else if(vis[v] && LOW[v]<LOW[x])
        {
            LOW[x]=LOW[v];
        }
    }
    if(DFN[x]==LOW[x])
    {
        cnt++;
        do
        {
            tmp=stack[top--];
            vis[tmp]=false;
            color[tmp]=cnt;
        }while (tmp!=x);
    }
}

void solve()
{
    int v,mark,num,ans;

    for(int i=1;i<=n;i++)
    {
        if(!DFN[i])
        Trajan(i);
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=head[i];j;j=Edge[j].next)
        {
            v=Edge[j].v;
            if(color[i]!=color[v])
            OutDegree[color[i]]++;
        }
    }
    num=0; mark=-1;
    for(int i=1;i<=cnt;i++)
    {
        if(!OutDegree[i])
        {
            num++; mark=i;
        }
    }
    ans=0;
    if(num!=1)
    {
        printf("0\n");
    }
    else
    {
        for(int i=1;i<=n;i++)
        {
            if(color[i]==mark)
            {
                ans++;
            }
        }
        printf("%d\n",ans);
    }
}

int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        init();
        read();
        solve();
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值