洛谷P2341 [USACO03FALL] 受欢迎的牛 G

发布时间 2023-09-24 23:13:50作者: xuantianhao

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;
}