受欢迎的牛

受欢迎的牛


题目描述

image-20210730155213679


核心思路

tarjan算法

题意:求出被所有牛(除自己外)都喜欢的”明星“数量。

有向图的强连通分量有个非常重要的作用:可以把一张图转变为有向无环图DAG。而求解有向图的强连通分量可以使用tarjan算法。

因为,当我们求出一个强连通分量时,可以把这个强连通分量进行缩点操作,缩成一个点,然后再把这个节点与其他节点连边,这样就可以得到一张DAG图。既然是有向无环图,那么就可以运用拓扑排序算法了。但是其实并不需要进行拓扑排序,因为tarjan算法本质是递归,当到了叶子节点时,算出一个连通分量,给它一个编号num,那么叶子节点所在的这个连通分量就是第一个,即num=1。然后回溯,再回溯过程中又求出了另一些连通分量,此时num++,也就是说,从后往前强连通分量的编号是递增的,这非常像DFS版的拓扑排序。而DFS版的拓扑排序的话,就是逆序输出就可以得到从前往后的拓扑序列了。对于tarjan来说,其实也是类似的,我们只需要从大到小输出强连通分量的编号num即可,因此不需要进行拓扑排序了。

对于这题来说,我们发现,使用tarjan算法后得到一张DAG图,如果图中出度为0的节点个数>1,那么则无解了,如果图中出度为0的节点个数=1,则有解。

如下图所示:

如果 A A A认为 B B B受欢迎,则从 A A A B B B连一条有向边

image-20210730161045557

那么该如何统计结果呢?如上图,这里3是指缩点后的节点3,它其实代表的是第3个强连通分量,假设是 1 → 2 → 3 1\to2\to3 123,可以知道第3个强连通分量中的牛都受到欢迎。因此,我们只需要统计出度为0的那个强连通分量中的牛的数量,那么就是答案了。

这题我们并不需要把缩点后的图建立出来,我们只需要求出每一个强连通分量,并给它们编上号。对于两个节点 i , k i,k i,k i i i k k k是邻接点的关系,假设 i → k i\to k ik,我们先查看它们属于哪一个强连通分量,我们用数组id[]来记录某个节点属于哪个强连通分量。假设节点 i i i属于强连通分量 a = i d [ i ] a=id[i] a=id[i],节点 k k k属于强连通分量 b = i d [ k ] b=id[k] b=id[k],那么有两种情况:

  • 如果 a = b a=b a=b,则说明节点 i , k i,k i,k属于同一个强连通分量,由于我们把一个强连通分量看成了一个缩点,那么它里面的节点就不需要谈论出度了。
  • 如果 a ≠ b a\neq b a=b,则说明节点 i , k i,k i,k不属于同一个强连通分量,那么统计强连通分量 a a a的出度,即 d [ a ] d[a] d[a]+ +

代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e4+10,M=5e4+10;
int n,m;
int h[N],e[M],ne[M],idx;
//表示节点u深度优先遍历的序号(也就是节点u被访问的时间点)
//表示节点u或节点u的子孙能够通过非父子边追溯到的dfn最小的节点序号
//即回到最早的过去(也就是节点u通过有向边可回溯到的最早的时间点)
//num表示时间戳
int dfn[N],low[N],num;
//a=id[x]表示x这个节点属于a这个强连通分量
//cnt[i]=100表示i这个强连通分量有100个节点
//scc表示强连通分量的编号 scc=3表示第3个强连通分量 最终也只有scc个强连通分量
int id[N],cnt[N],scc;
//栈用来存储访问的节点  top是栈顶指针
int stk[N],top; 
//存储每个节点的出度
int dout[N];
//in_stk[i]=true表示节点i还在栈中
bool in_stk[N];

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

void tarjan(int u)
{
    dfn[u]=low[u]=++num;    //给节点u分配一个时间戳
    stk[++top]=u;           //将节点u入栈
    in_stk[u]=true;         //标记节点u在栈中
    //遍历节点u的所有邻接点
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i]; //u的邻接点j
        //根据分析得出:先会执行else if中的更新语句,才会执行if中的回溯过程中的更新语句
        //如果节点j还没有被访问过
        if(!dfn[j])
        {
            tarjan(j);//递归访问j
            //回溯是更新从节点j往回到节点u这条路径上的所有节点的low值
            low[u]=min(low[u],low[j]);
        }
        //否则说明节点j已经被访问过了 看j是否还在栈中
        //如果还在栈中 那么就说明形成了环 即存在强连通分量
        //当走到这里时说明j先于u被访问,因此u可以追溯到更早的过去
        //于是用更早的dfn[j]来更新low[u]
        else if(in_stk[j])
            low[u]=min(low[u],dfn[j]);
    }
    //退无可退 即此时u不能追溯到更早的过去了 那么它就是这个强连通分量的入口
    if(dfn[u]==low[u])
    {
        scc++;  //强连通分量的个数+1
        int y;
        //输出这个强连通分量中所包含的节点
        do{
            y=stk[top--];       //强连通分量的节点y
            in_stk[y]=false;    //标记节点y不在栈中
            id[y]=scc;      //节点y属于scc这个强连通分量
            cnt[scc]++;     //scc这个强连通分量中的节点个数增加了y这个节点
        }while(y!=u);
    }
}

int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }

    //执行tarjan算法
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    
    //统计新图中点的出度 
    for(int i=1;i<=n;i++)
    {
        //遍历节点i的所有邻接点
        for(int j=h[i];~j;j=ne[j])
        {
            int k=e[j]; //节点i的邻接点k
            //查看节点i和节点k分别属于哪个强连通分量
            int a=id[i];
            int b=id[k];
            //如果它俩属于不同的强连通分量,由于是i>k
            //所以a这个强连通分量的出度+1
            if(a!=b)
                dout[a]++;
        }
    }
    //zeros记录出度为0的节点个数
    //sum存储所有出度为0的强连通分量的点的数量
    int zeros=0,sum=0;
    for(int i=1;i<=scc;i++)
    {
        //如果第i个强连通分量出度为0
        if(!dout[i])
        {
            zeros++;
            sum+=cnt[i];    //加上第i个强连通分量的点的个数
            //如果存在出度为0的节点个数>1,则某一头牛必然不会受到另一头牛的喜欢
            //与题意不符合
            if(zeros>1)
            {
                sum=0;
                break;
            }
        }
    }
    printf("%d\n",sum);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值