洛谷P2341 [USACO03FALL] 受欢迎的牛 G
P2341 受欢迎的牛 G 题解
这题我们需要了解强连通分量
一、定义
在有向图 \(G\) 中,如果两个顶点 \(u\) , \(v\) 间有一条从 \(u\) 到 \(v\) 的有向路径,同时还有一条从 \(v\) 到 \(u\) 的有向路径,则称两个顶点强连通。如果有向图 \(G\) 的每两个顶点都强连通,称 \(G\) 是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。
二、 \(tarjan\) 算法 时间复杂度是 \(O(N+M)\)
四条边:
树枝边: \(DFS\) 时经过的边,即 \(DFS\) 搜索树上的边。
前向边:与 \(DFS\) 方向一致,从某个结点指向其某个子孙的边。
后向边:与 \(DFS\) 方向相反,从某个结点指向其某个祖先的边。(返祖边)
横叉边:从某个结点指向搜索树中的另一子树中的某结点的边。
思想:
\(Tarjan\) 算法是基于对图深度优先搜索的算法
每个强连通分量为搜索树中的一棵子树
搜索时,把当前搜索树中未处理的节点加入一个堆栈
回溯时可以判断栈顶到栈中的节点是否为一个强连通分量
定义 \(DFN(u)\) 为节点 \(u\) 搜索的次序编号(时间戳)
\(LOW(u)\) 为 \(u\) 或 \(u\) 的子树能够追溯到的最早的栈中节点的次序号
由定义可以得出:
\(Low(u)=Min (Low(u), Low(v))\)
\((u,v)\) 为树枝边, \(u\) 为 \(v\) 的父节
\(Low(u)=Min (Low(u), DFN(v))\)
\(DFN(v)\) , \((u,v)\) 为指向栈中节点的后向边(指向栈中结点的横叉边)
当结点 \(u\) 搜索结束后
若 \(DFN(u)=Low(u)\) 时,则以 \(u\) 为根的搜索子树上所有还在栈中的节点是一个强连通分量。
剩下的具体细节见代码内注释(超详细)
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+100;
const int M=5e4+100;
int to[M],nex[M],head[M];
//建图数组
int dfn[N];
//结点u的搜索次序编号
int low[N];
//从u出发能遍历到的最小结点编号
int de[N];
//每个点入度个数
int si[N];
//每个强连通分量中集合个数
int co[N];
//表示分成几个强连通分量
stack<int> s;
int n,m,ans,top,tot;
int x,y,d,cnt,num;
void tarjan(int u){
//tarjan缩点,寻找强连通分量
dfn[u]=low[u]=++num;
//设置初值,num是时间戳
s.push(u);
//将u入栈
for(int i=head[u];i!=-1;i=nex[i]){
//枚举u的所有出边(u,v)
int v=to[i];
if(!dfn[v]){
//结点v未被访问过
tarjan(v);
//搜索遍历
low[u]=min(low[u],low[v]);
//更新low[u]
}else if(!co[v]){
//点v在栈中
low[u]=min(low[u],dfn[v]);
//更新low[u]
}
}
if(low[u]==dfn[u]){
co[u]=++cnt;
//记录第几个集合
si[cnt]=1;
//记录一个强连通分量中集合数量
while(s.top()!=u){
//u和栈中在u之后所有结点构成一个强连通分量
co[s.top()]=cnt;
//st[top]为该强连通分量中的结点
si[cnt]++;
//增加一个集合个数
s.pop();
//退栈
}
s.pop();
//将u退栈
}
}
int main(){
scanf("%d%d",&n,&m);
memset(head,-1,sizeof(head));
memset(low,0x7f/3,sizeof(low));
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
nex[i]=head[x];
to[i]=y;
head[x]=i;
}
for(int i=1;i<=n;i++){
if(!dfn[i]) num=0,tarjan(i);
//缩点
}
for(int i=1;i<=n;i++){
for(int j=head[i];j!=-1;j=nex[j]){
//统计入度
if(co[i]!=co[to[j]]){
//不在一个集合就统计入度
de[co[i]]++;
}
}
}
for(int i=1;i<=cnt;i++){
if(de[i]==0){
//记录入度为零的点,有多个则输出0
ans+=si[i];
d++;
}
}
if(d==1) printf("%d\n",ans);
else printf("0\n");
return 0;
}