受欢迎的牛
题目描述
核心思路
题意:求出被所有牛(除自己外)都喜欢的”明星“数量。
有向图的强连通分量有个非常重要的作用:可以把一张图转变为有向无环图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连一条有向边
那么该如何统计结果呢?如上图,这里3是指缩点后的节点3,它其实代表的是第3个强连通分量,假设是 1 → 2 → 3 1\to2\to3 1→2→3,可以知道第3个强连通分量中的牛都受到欢迎。因此,我们只需要统计出度为0的那个强连通分量中的牛的数量,那么就是答案了。
这题我们并不需要把缩点后的图建立出来,我们只需要求出每一个强连通分量,并给它们编上号。对于两个节点
i
,
k
i,k
i,k,
i
i
i和
k
k
k是邻接点的关系,假设
i
→
k
i\to k
i→k,我们先查看它们属于哪一个强连通分量,我们用数组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;
}