Popular Cows POJ - 2186(tarjan算法)+详解

题意:

每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N头牛,给你M对整数(A,B),表示牛 A认为牛B受欢迎。这种关系是具有传递性的,如果 A认为 B受欢迎, B认为 C受欢迎,那么牛 A也认为牛 C受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

题目:

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

Hint

Cow 3 is the only cow of high popularity.

思路:强连通——tarjan算法

即建立一个搜索树,运用tarjan算法进行缩点,最终建成一棵新树。
1.所有点只建成一棵树(u<=1)。
2.该树只有两个结果,即连通(u=1)和不连通(u=0)
3.在ac代码后,我会详细介绍tarjan算法。

AC代码(带步骤解释)

#include<stdio.h>//tarjan是一个缩点过程。
#include<string.h>
#include<algorithm>
using namespace std;
const int M=1e4+10;
const int N=5e4+10;
int to[N],nex[N],fir[M];
int col,num,dfn[M]/*时间戳:标记当前节点在深搜过程中是第几个遍历到的点*/;
int low[M]/*整个算法核心数组:每个点在这颗树中的,最小的子树的根*/;
int de[M]/*统计新建树的入度*/,si[M]/*统计新树中某节点(强连通分量)内包含多少个点*/;
int tot=0,co[M]/*表示新树的元素*/,n,m;
int top,st[M]/*栈*/;
void Ins(int x,int y)
{
    to[++tot]=y;
    nex[tot]=fir[x];///模拟链表
    fir[x]=tot;
}
void tarjan(int u/*当前节点*/)///tarjan缩点
{
    dfn[u]=low[u]=++num;//初始化
    st[++top]=u;//将u节点入栈
    for(int i=fir[u]; i; i=nex[i]) ///枚举每一条边。
    {
        int v=to[i]/*其能到达的节点*/;
        if(!dfn[v])//如果v点未被访问过
        {
            tarjan(v);//继续往下找
            low[u]=min(low[u],low[v]);
        }
        else if(!co[v]) ///判断是否在新树中,若不在需要对改点值更新。
            low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u])//某个节点回溯之后的low【u】值还是==dfn【u】的值,那么这个节点无疑就是一个关键节点(为强连通分量的一个顶点。)
    {
        co[u]=++col;/*看做建了一个新树,只有用强连通分量的顶点建入树中*/
        ++si[col];/*记录某连通分量内有多少个点*/
        while(st[top]!=u)//until u==v;(遍历该连通分量内有多少个点【在u前的点,均为一个连通分量】)
        {
            ++si[col];
            co[st[top]]=col;//建立新树,该联通分量内均为同一个时间戳。
            --top;//比此节点后进来的节点全部出栈
        }
        --top;//将u退栈。
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=m; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        Ins(y,x);///反向建边,统计入度。
    }
    for(int i=1; i<=n; i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1; i<=n; i++)
        for(int j=fir[i]; j; j=nex[j]) ///统计新树入度。
        {
            int v=to[j]; //i - > v
            int U=co[i];
            int V=co[v];  // U -> V
            if(U!=V)//前面操作,使得联通分量内均为同一个时间戳(最小时间戳)
                de[V]++;
        }

    int ans=0,u=0;
    for(int i=1; i<=col; i++)
        if(!de[i])//入度不为零
            ans=si[i],u++;//此时新树中无连通分量
    if(u==1)//表明所有牛都欢迎
        printf("%d\n",ans);//(该新树联通分量包含几个点)
    else printf("0\n");//u==0,该树不连通;u>1,有多个树。

    return 0;
}

tarjan算法详解(以此题求解过程为例)

不知道怎样读tarjan,就去搜了一下,发现发明算法的不是中国人(挺正常?) 发明者:Robert Tarjan,所以我还是老老实实的叫塔尖算法吧。
在这里插入图片描述
看他和(残)蔼(酷)的脸,他发明的 tarjan算法,是一个关于图的联通性(将强连通分量【一堆点】)缩成一个点)的神奇算法。
基于DFS(迪法师)算法,深度优先搜索一张有向图。!注意!是有向图。根据树,堆栈,领接表等种种神奇方法来完成剖析一个图的工作。最后经过这些操作建成一个新树的过程。(缩点过程)思维千丝万缕,但代码却不长(妙哉)?orz废话不多说,来说下我理解的tarjan(说了,以此题为例)。

首先我们引入定义:

1、有向图G中,以顶点v为起点的弧的数目称为v的出度;以顶点v为终点的弧的数目称为v的入度。
2、如果在有向图G中,有一条<u,v>有向道路,则v称为u可达的,或者说,从u可达v。
3、如果有向图G的任意两个顶点都互相可达,则称图 G是强连通图,如果有向图G存在两顶点u和v使得u不能到v,或者v不能到u,则称图G是强非连通图。
4、如果有向图G不是强连通图,他的子图G2是强连通图,点v属于G2,任意包含v的强连通子图也是G2的子图,则乘G2是有向图G的极大强连通子图,也称强连通分量。
5、什么是强连通?强连通其实就是指图中有两点u,v。使得能够找到有向路径从u到v并且也能够找到有向路径从v到u,则称u,v是强连通的。

不妨引入一个图加强大家对强连通分量和强连通的理解:

在这里插入图片描述
标注棕色线条框框的三个部分就分别是一个强连通分量,也就是说,这个图中的强连通分量有3个。
其中我们分析最左边三个点的这部分:
其中1能够到达0,0也能够通过经过2的路径到达1.1和0就是强连通的。
其中1能够通过0到达2,2也能够到达1,那么1和2就是强连通的。

同理,我们能够看得出来这一部分确实是强连通分量,也就是说,强连通分量里边的任意两个点,都是互相可达的。
那么如何求强连通分量的个数呢?另外强连通算法能够实现什么一些基本操作呢?

即如何用Tarjan算法求强连通分量个数:

先来段伪代码
tarjan官方伪代码如下:

//parent为并查集,FIND为并查集的查找操作

//QUERY为询问结点对集合

//TREE为基图有根树

Tarjan(u)

visit[u] = true

for each (u, v) in QUERY

if visit[v]

ans(u, v) = FIND(v)

for each (u, v) in TREE

if !visit[v]

Tarjan(v)

parent[v] = u

本题伪代码
tarjan(u){

  DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值

  Stack.push(u)   // 将节点u压入栈中

  for each (u, v) in E // 枚举每一条边

    if (v is not visted) // 如果节点v未被访问过

        tarjan(v) // 继续向下找

        Low[u] = min(Low[u], Low[v])

    else if (v in S) // 如果节点u还在栈内

        Low[u] = min(Low[u], DFN[v])

  if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根

  repeat v = S.pop  // 将v退栈,为该强连通分量中一个顶点

  print v

  until (u== v)

}

Tarjan算法,是一个基于Dfs的算法,假设我们要先从0号节点开始Dfs,我们发现一次Dfs我萌就能遍历整个图(树),而且我们发现,在Dfs的过程中,我们深搜到 了其他强连通分量中,那么如何判断哪个和那些节点属于一个强连通分量呢?我们首先引入两个数组:

①dfn[ ]
②low[ ]

第一个数组dfn我们用来标记时间戳:当前节点在深搜过程中是第几个遍历到的点
第二个数组是整个算法核心数组:经过运算后在这颗树中的,强连通分量内的的点的low【u】值均为强连通分量顶点的时间戳(以判断low【u】的值是否一样来判断是否为同一个强连通变量)
这个时候我们不妨在纸上画一画写一写,搞出随意一个Dfs出来的dfn数组来观察一下(假设我们从节点0开始的Dfs,其中一种可能的结果是这样滴):
在这里插入图片描述
这个时候我们回头来看第二个数组要怎样操作,我们定义low【u】=min(low【u】,low【v】(即使v搜过了也要进行这步操作,但是v一定要在栈内才行)),

  • (1) u代表当前节点,v代表其能到达的节点。

  • (2)这个数组在刚刚到达节点u的时候初始化low【u】=dfn【u】

  • (3)然后在进行下一层深搜之后回溯回来的时候,维护low【u】。
    1)如果如果v点未被访问过Low[u] = min(Low[u], Low[v])
    2)如果如果v点被访问过,且未被建在新树中。

  • (4)如果我们发现了某个节点回溯之后的**low【u】值==dfn【u]**的值,那么这个节点无疑就是一个关键节点(为强连通分量的一个顶点):从这个节点能够到达其强连通分量中的其他节点,但是没有其他属于这个强连通分量以外的点能够到达这个点,所以这个点的low【u】值维护完了之后还是和dfn【u】的值。

上图运行一遍的各个数值的变化。

①首先进入0号节点,初始化其low【0】=dfn【0】=1,然后深搜到节点2,初始化其:low【2】=dfn【2】=2,然后深搜到节点1,初始化其:low【1】=dfn【1】=3,将其全部进栈;

②然后从节点1开始继续深搜,发现0号节点已经搜过了,没有继续能够搜的点了,开始回溯维护其值。low【1】=min(low【1】,low【0】)=1;low【2】=min(low【2】,low【1】)=1;low【0】=min(low【0】,low【2】)=1;

③这个时候虽然low【0】==dfn【0】,但不能断定0号节点是一个关键点,别忘了,这个时候还有3号节点没有遍历,我们只有在其能够到达的节点全部判断完之后,才能够下结论,所以我们继续Dfs。

④继续深搜到3号节点,初始化其low【3】=dfn【3】=4,然后深搜到4号节点,初始化其:low【4】=dfn【4】=5,将两个点都进栈。这个时候发现深搜到底,回溯,因为节点4没有能够到达的点,所以low【4】也就没有幸进行维护即:low【4】=dfn【4】出栈,进入新树co[4]=1(这个点一定是强连通分量的关键点,但是我们先忽略这个点,这个点没有代表性,一会分析关键点的问题),然后回溯到3号节点,low【3】=min(low【3】,low【4】)=4;(由于连通性质,回溯时,low【3】的时间戳一定小于low【4】,low【3】是一个关键点,不会改变)发现low【3】==dfn【3】出栈,co[3]=2(那么这个点也是个关键点,我们同样忽略掉。)

⑤最终回溯到节点0,进行最后一次值的维护:low【0】=min(low【0】,low【3】)=0,这个时候我们猛然发现其dfn【0】==low【0】,根据刚才所述,那么这个点就是一个关键点:能够遍历其属强连通分量的点的起始点,而且没有其他点属于其他强连通分量能够有一条有向路径连到这个节点来的节点,此时栈顶元素为2,则栈内在0上的所有点包括零都为同一个强连通分量,出栈进入新树co[2]=3,co[1]=3,co[0]=3.

大家仔细理解一下这句话,因为这个点属于一个强连通分量,而且强连通分量中的任意两个节点都是互达的,也就是说强连通分量中一定存在环,这个最后能够回到0号节点的1号节点一定有机会维护low【1】,因为0号节点是先进来的时间戳一定小,所以其low【1】的值也一定会跟着变小,然后在回溯的过程中,其属一个强连通分量的所有点都会将low【u】值维护成low【0】,所以这个0号节点就是这个关键点:能够遍历其属强连通分量的起始点而且这样的起始点一定只有一个,所以只要发现了一个这样的关键起始点,那么就一定发现了一个强连通分量。而且这个节点没有其他点属于其他强连通分量能够有一条有向路径连到这个节点来的节点:如果这样的点存在,那么这些个点应该属于同一个强连通分量。

那么综上所述,相信大家也就能够理解为什么dfn【u】==low【u】的时候,我们就可以判断我们发现了一个强连通分量了。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值