【暖*墟】 #图论# 强连通分量与缩点(tarjan)

5 篇文章 0 订阅
1 篇文章 0 订阅
本文详细介绍了Tarjan算法用于寻找有向图中的强连通分量(SCC)。文章从强连通分量的基本概念出发,解释了tarjan算法的实现过程,包括dfn、low、stack和vis数组的使用。同时,通过多个例题分析,如受欢迎的牛、HXY烧情侣等,展示了如何应用算法解决实际问题,涉及有向图的缩点、连通性判断和入度出度判断等技巧。
摘要由CSDN通过智能技术生成

tarjan求有向图强连通分量(scc)

        【一. 常见概念理解】

【二. 常见数组及其使用】

【三. tarjan算法的实现过程】

【四. 常见例题分析】


【一. 常见概念理解】

  • 强连通分量:任意两个节点相互可达的强连通图中最大的一个。
  • tarjan:一个类似dfs的过程,用于寻找有向图中的强连通分量。
  • 缩点:对于具有传导性的关系,把有向图中的"环"缩成点,形成有向无环图。

求强连通分量:先把有向图看成一棵有向树,进行dfs(tarjan)。

当有节点的连边连向dfn比它小的地方时,就形成了环,需要判断连通分量了。


【二. 常见数组及其使用】

(1)、dfn[i],表示时间戳,即这个点在dfs时是第几个被搜到的。

(2)、low[i],表示[这个点以及其子孙节点]连的所有点中[dfn最小的值],

                   即:能匹配到的最上层的祖先的位置。

(3)、stack[i],表示当前过程中可能构成强连通分量的所有点。

(4)、vis[i],表示一个点是否在stack[i]数组中。


【三. tarjan算法的实现过程】

从点u开始遍历(某个dfn为0的节点):

(1)首先初始化:dfn[u]=low[u]=第几个被dfs到。

(2)将u存入stack[]中,并将vis[u]设为true。

--- stack[ ]的意义是:如果u在stack中,在u被回溯时、

u和栈中所有在它之后的点、都可以构成强连通分量。

(3)遍历u的所有连向点v,如果dfn[v]为0,即未访问过,就对点v进行dfs

在遍历子节点的连向边的过程中,如果遍历到dfn比它小的地方

判断该连向节点是否在stack[ ]里,如果在,更新 low[v]。

low[u]=min{low[u],low[v]};

--- low[ u ]的意义是:记录该点能连通到的最大的祖先节点(或者自己),

即能连通到的dfn最小的值,且该值在栈内(保证是u的祖先)。

(4)如果dfn[u]=low[u],说明 [ u点及u点之下的所有子节点 ] 没有边会指向u的祖先,

那么u就是强连通分量的顶端,记录此次搜索到的强连通分量。

把所有的u点及以后压入栈中的所有点弹出,vis[ ]改为false(或打上相同标记)。


【四. 常见例题分析】

       目录  【例题1】【p2341】受欢迎的牛

              【例题2】【p2194】HXY烧情侣

              【例题3】【p2416】泡芙

              【例题4】【p2002】消息扩散

              【例题5】【p2863】牛的舞会

              【例题6】【p1262】间谍网络

              【例题7】【p3043】牛联盟


【例题1】【p2341】受欢迎的牛

  • 告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,
  • 如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。

【标签】有向图缩点 + 连通性判断 + 入度出度的判断

将整张图(上的强连通分量)进行缩点,剩下的点都不强连通,整张图变成了有向无环图。

有向无环图上必然有>=1个的点出度为0,如果>1个,那么图不相连、答案为0。

如果只有一个点出度为0,判断它是不是强连通分量缩成的点,输出包含的点数。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p2341】受欢迎的牛
告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,
如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。*/

//将整张图(上的强连通分量)进行缩点,剩下的点都不强连通,整张图变成了有向无环图。
//有向无环图上必然有>=1个的点出度为0,如果>1个,那么图不相连、答案为0。
//如果只有一个点出度为0,判断它是不是强连通分量缩成的点,输出包含的点数。

const int N=500019;

ll n,m,x,y,tot=0,head[N*2],tmp=0,ans=0;

ll dfss=0,num=0,sum=0,col[N],times[N],du[N];

ll dfn[N],low[N],stack[N],vis[N]; //tarjan算法中的四个数组

struct node{ ll ver,nextt; }e[N*2];

void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(ll u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        col[u]=++sum; vis[u]=0;
        while(stack[num]!=u){ //u上方的节点是可以保留的
            col[stack[num]]=sum;
            vis[stack[num]]=0,num--;
        } num--; //col数组记录每个点所在连通块的编号
    }
}

int main(){
    while(scanf("%lld%lld",&n,&m)!=EOF){
        memset(vis,false,sizeof(vis));
        memset(dfn,0,sizeof(dfn)); 
        dfss=0,num=0,tot=0,sum=0,ans=0,tmp=0;
        memset(head,0,sizeof(head));
        for(ll i=1;i<=m;i++){
            scanf("%lld%lld",&x,&y),add(x,y);
        } for(ll i=1;i<=n;i++)
            if(!dfn[i]) tarjan(i);
        for(ll u=1;u<=n;u++){
            for(ll i=head[u];i;i=e[i].nextt)
                if(col[e[i].ver]!=col[u]) du[col[u]]++;
            times[col[u]]++; //记录强连通分量大小
        } for(ll i=1;i<=sum;i++) //对每种颜色(缩点得到的点)
            if(du[i]==0) tmp++,ans=times[i];
        if(tmp==0||tmp>1) cout<<0<<endl;
        else cout<<ans<<endl;
    }
}

【例题2】【p2194】HXY烧情侣

  • n个点,m条有向边,每个点都有点权w[i]。
  • 如果能从一个点出发并返回这个点,只需要出发点的价值。
  • 希望遍历所有的点,询问最少价值和最少价值的方案数。

【标签】有向图缩点 + 乘法原理求方案数

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p2194】HXY烧情侣
n个点,m条有向边,每个点都有点权w[i]。
如果能从一个点出发并返回这个点,只需要出发点的价值。
遍历所有的点,询问最少价值和最少价值的方案数。*/

//寻找强连通分量 + 乘法原理求方案数

const int N=500019,mod=1000000007;

ll n,m,ans1,ans2=1,tot=0,head[N*2],w[N];

ll dfn[N],low[N],stack[N],vis[N],dfss=0,num=0; 
//↑↑tarjan算法中的 四个数组 和 两个变量

struct node{ ll ver,nextt; }e[N*2];

void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(ll u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){ ll minn=(ll)1e15,cnt=0;
        do{ vis[stack[num]]=0; //以后这里的判断全部用do while...
            int now=w[stack[num]]; num--; //此点的价值
            if(now==minn) cnt++; //此连通分量的最小价值的方案数
            if(now<minn) minn=now,cnt=1; //有更小的,cnt还原为1
        }while(stack[num+1]!=u); //更新答案一定要包含节点u啊...
        ans1+=minn; ans2=(ans2*cnt)%mod; //用乘法原理求方案数
        //↑↑记录此连通分量(包含只有一个节点的情况)的最小权值 和 方案数
    }
}

int main(/*hs_love_wjy*/){
    ll x,y; cin>>n; for(ll i=1;i<=n;i++) cin>>w[i];
    cin>>m; for(ll i=1;i<=m;i++) cin>>x>>y,add(x,y);
    for(ll i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    cout<<ans1<<" "<<ans2<<endl; //最小代价和方案数
}

【例题3】【p2416】泡芙

  • 输入位置st、ed,询问路径上是否经过权值为1的边。

【标签】无向图缩点 + 无向无环图性质(就是树) + lca

先放一份自己的代码...觉得并没有什么问题但是TLE了...

果然还是我太水了 不过这种题了解写法了解思维就够了吧qwq

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p2416】泡芙
输入位置st、ed,输出路径上是否经过权值为1的边。*/

//无向图缩点 + 无向无环图性质(就是树) + lca

void reads(int &x){ //读入优化(正负整数)
    int fa=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fa; //正负号
}

const int N=800019;

int n,m,q,tot=0,head[N*2],tot1=0,head1[N*2];

int dep[N],dist[N],fa[N][20],col[N],sizes[N],circle[N];

int dfn[N],low[N],stack_[N],dfss=0,num=0,sum=0; 
//↑↑tarjan算法中的 四个数组 和 三个变量

struct node{ int ver,nextt,w; }e[N*2],e1[N*2];

void add(int u,int v,int w){ e[++tot].ver=v,e[tot].nextt=head[u],e[tot].w=w,head[u]=tot; }

void add1(int u,int v,int w){ e1[++tot1].ver=v,e1[tot1].nextt=head1[u],e1[tot1].w=w,head1[u]=tot1; }

void tarjan(int u,int u_fa){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,stack_[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(e[i].ver==u_fa) continue; //【无向图缩点】
        if(!dfn[e[i].ver]) tarjan(e[i].ver,u),low[u]=min(low[u],low[e[i].ver]);
        else if(!col[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        col[u]=++sum,sizes[sum]=1;
        while(stack_[num]!=u){
            col[stack_[num]]=sum;
            sizes[sum]++,num--;
        } num--; //记录所有的连通分量和它们的大小
    }
}

void sum_circle(){ //预处理每个环的边权sum,并重新建图
    for(int x=1;x<=n;x++){
        for(int i=head[x];i;i=e[i].nextt){
            if(col[x]!=col[e[i].ver]) 
                add1(col[x],col[e[i].ver],e[i].w),add1(col[e[i].ver],col[x],e[i].w);
            else circle[col[x]]+=e[i].w;
        } //↑↑建立新图,每个连通分量的边权记录在编号上
    }
}

void pre_dfs(int u,int u_fa){ //缩点之后变成了无向无环图,也就是树
    for(int i=0;i<20;i++) fa[u][i+1]=fa[fa[u][i]][i];
    for(int i=head1[u];i;i=e1[i].nextt){
        int v=e1[i].ver; //找到下一条相连的边
        if(v==u_fa) continue;
        dep[v]=dep[u]+1; //深度
        dist[v]=dist[u]+e1[i].w+circle[v]; //距离
        fa[v][0]=u; pre_dfs(v,u); //记录father,递归
    }
}

int lca_(int x,int y){ //找lca的主程序
    
    if(dep[x]<dep[y]) swap(x,y); //保证dep[x]>dep[y]
    
    for(int i=19;i>=0;i--){ //注意:这里的20和上面的19都是log2n的近似取值
        if(dep[fa[x][i]]>=dep[y]) x=fa[x][i]; 
        //↑↑↑i的2^k辈祖先的结点仍比y深,令x=fa[x,i],继续向上跳
        if(x==y) return x; //若x=y,则已经找到了lca
    }

    for(int i=19;i>=0;i--) //↓↓↓未找到lca时,一起倍增
        if(fa[x][i]!=fa[y][i]){ x=fa[x][i]; y=fa[y][i]; } 

    return fa[x][0]; //最终的答案为fa[x][0]
}

int main(/*hs_love_wjy*/){
    int x,y,z,ans; cin>>n>>m;
    for(int i=1;i<=m;i++){
        reads(x),reads(y),reads(z);
        add(x,y,z); add(y,x,z); //无向图啊...
    } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
    sum_circle(); //预处理每个环的边权sum,环上任意点是相互可达的 
    dist[col[1]]=circle[col[1]]; pre_dfs(col[1],0);
    cin>>q; for(int i=1;i<=q;i++){
        reads(x),reads(y); x=col[x],y=col[y];
        int lca=lca_(x,y); ans=dist[x]+dist[y]-2*dist[lca]+circle[lca];
        if(ans>=1) cout<<"YES"<<endl; else cout<<"NO"<<endl;
    }
}

然后是AC代码...思路一样的(受教qwq)...

#include <iostream>
#include <cstdio>

using namespace std;

const int wx=800017;

inline int read(){
    int sum=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=(sum<<1)+(sum<<3)+ch-'0'; ch=getchar();}
    return sum*f;
}

int a[wx],belong[wx],dfn[wx],low[wx],size[wx];
int head[wx],h[wx],st[wx],dep[wx],dis[wx];
int f[wx][23];
int n,m,q;
int top,tot,num,Num,col;

struct e{
    int nxt,to,dis;
}edge[wx*2];

struct ee{
    int nxt,to,dis;
}e[wx*2];

void Add(int from,int to,int dis){
    e[++Num].nxt=h[from];
    e[Num].to=to;
    e[Num].dis=dis;
    h[from]=Num;
}

void add(int from,int to,int dis){
    edge[++num].nxt=head[from];
    edge[num].to=to;
    edge[num].dis=dis;
    head[from]=num;
}

void Tarjan(int u,int fa){
    dfn[u]=low[u]=++tot;
    st[++top]=u;
    for(int i=h[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa)continue;
        if(!dfn[v]) Tarjan(v,u), low[u]=min(low[u],low[v]);
        else if(!belong[v]) low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]){
        belong[u]=++col;
        size[col]=1;
        while(st[top]!=u){
            belong[st[top]]=col;
            size[col]++;
            top--;
        }
        top--;
    }
}

void CQ(){
    for(int u=1;u<=n;u++){
        for(int i=h[u];i;i=e[i].nxt){
            int v=e[i].to;
            if(belong[u]!=belong[v])
                add(belong[u],belong[v],e[i].dis);
            else a[belong[u]]+=e[i].dis;
        }
    }
}

void dfs(int u,int fa){
    dep[u]=dep[fa]+1;
    for(int i=head[u];i;i=edge[i].nxt){
        int v=edge[i].to;
        if(v==fa)continue;
        f[v][0]=u; dis[v]=dis[u]+edge[i].dis+a[v];
        dfs(v,u);
    }
}

void pre(){
    for(int j=1;j<=20;j++)
        for(int i=1;i<=n;i++)
            f[i][j]=f[f[i][j-1]][j-1];
}

int LCA(int x,int y){
    if(dep[x]<dep[y])swap(x,y);
    for(int i=20;i>=0;i--)
        if(dep[f[x][i]]>=dep[y])x=f[x][i];
    if(x==y)return x;
    for(int i=20;i>=0;i--)
        if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
    return f[x][0];
}

int main(){
    n=read(); m=read();
    for(int i=1;i<=m;i++){
        int x,y,z; 
        x=read(); y=read(); z=read();
        Add(x,y,z); Add(y,x,z);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i,0);
    CQ();
    dis[belong[1]]=a[belong[1]];
    dfs(belong[1],0); pre();
    q=read();
    for(int i=1;i<=q;i++){
        int x,y;
        x=read(); y=read();
        x=belong[x]; y=belong[y];
        int lca=LCA(x,y);
        if(dis[x]+dis[y]-2*dis[lca]+a[lca]>0)puts("YES");
        else puts("NO");
    }
    return 0;
}

【例题4】【p2002】消息扩散

  • 有向图。n个城市,m条单行道路。道路可能重复或者自环。
  • 至少需要在几个城市发布消息才能让这所有n个城市都得到消息。

【标签】思维 + 有向图强连通分量 + 特判自环情况

题意可以转化为:统计入度为0的强连通分量的个数。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p2002】消息扩散
有向图。n个城市,m条单行道路。道路可能重复或者自环。
至少需要在几个城市发布消息才能让这所有n个城市都得到消息。*/

//统计入度为0的强连通分量的个数

void reads(int &x){ //读入优化(正负整数)
    int fa=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fa; //正负号
}

const int N=800019;

int n,m,q,tot=0,head[N*2],col[N],rd[N];

int dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0; 
//↑↑tarjan算法中的 四个数组 和 三个变量

struct node{ int ver,nextt; }e[N*2];

void add(int u,int v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(int u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        col[u]=++sum; vis[u]=0;
        while(stack_[num]!=u){
            col[stack_[num]]=sum;
            vis[stack_[num]]=0,num--;
        } num--; //记录所有的连通分量
    }
}

int main(/*hs_love_wjy*/){
    int x,y,ans=0; cin>>n>>m;
    for(int i=1;i<=m;i++){ //↓↓特判自环的情况
        reads(x),reads(y); if(x!=y) add(x,y);
    } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
    for(int x=1;x<=n;x++)
        for(int i=head[x];i;i=e[i].nextt)
            if(col[x]!=col[e[i].ver]) rd[col[e[i].ver]]++;
    for(int i=1;i<=sum;i++) if(!rd[i]) ans++;
    cout<<ans<<endl; return 0;
}

【例题5】【p2863】牛的舞会

  • 给你n个点,m条边,求图中所有大小大于1的强连通分量的个数。

【标签】强连通分量(模板题) + 计数

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

//【p2863】给你n个点,m条边,求图中所有大小大于1的强连通分量的个数

void reads(ll &x){
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+(s-'0');s=getchar();}
    x=x*fx; //正负号
}

const int N=500019;

ll n,m,tot=0,head[N*2],tmp[N],ans=0,dfss=0,num=0; //tmp和ans都用于记录答案

ll dfn[N],low[N],stack[N],vis[N]; //tarjan算法中的四个数组

struct node{ ll ver,nextt; }e[N*2];

void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(ll u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=1,stack[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]);
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        ll cnt=0; //此次构成的连通分量的大小
        while(stack[num]!=u){ //u上方的节点是可以保留的
            cnt++,vis[stack[num]]=0,num--;
        } cnt++,vis[u]=0,num--;
        if(cnt>1) ans++;
    }
}

int main(){
    ll x,y; reads(n),reads(m);
    for(ll i=1;i<=m;i++){
        reads(x),reads(y),add(x,y);
    } for(ll i=1;i<=n;i++) //注意,dfn为0时,
        if(!dfn[i]) tarjan(i); //才需要重新调用tarjan函数
    printf("%lld\n",ans); //强连通分量的个数
}

【例题6】【p1262】间谍网络

  • 每个人掌握一些情报,对于情报中提及的每个人,能获得他有的情报。
  • 有一些人可以被收买。希望获得所有人的情报,求min代价。

【标签】思维 + 分情况讨论 + 有向图缩点

【分析】因为关系有传递性-->有向图-->1.不连通,无解;

2.连通,无环-->代价为rd=0的点的权值,有环-->代价为缩点后rd=0的点的权值。

可以直接把三种情况都归到有环的情况中,先缩点,再计算入度、得到ans。

P.S.本蒟蒻一开始想要分情况讨论,写了一个have数组表示是否被传递到了。

但是我没有dfs,直接用连向边瞎搞的...结果特判失败被打回原形2333...

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p1262】间谍网络
每个人掌握一些情报,对于情报中的每个人,能获得他的情报。
希望获得所有人的情报,求min代价。*/

//【标签】思维 + 分情况讨论 + 有向图缩点

/*【分析】因为关系有传递性-->有向图-->1.不连通,无解;
2.连通,无环-->代价为rd=0的点的权值,有环-->代价为缩点后rd=0的点的权值。
可以直接把三种情况都归到有环的情况中,先缩点,再计算入度、得到ans。*/

void reads(int &x){ //读入优化(正负整数)
    int fa=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fa; //正负号
}

const int N=500019;

int n,p,m,tot=0,head[N*2],col[N],rd[N],a[N],circle[N];

int dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0; 
//↑↑tarjan算法中的 四个数组 和 三个变量

struct node{ int ver,nextt; }e[N*2];

void add(int u,int v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(int u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
    
    for(int i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        col[u]=++sum; vis[u]=0; circle[sum]=a[u];
        while(stack_[num]!=u){
            circle[sum]=min(circle[sum],a[stack_[num]]),
            col[stack_[num]]=sum,vis[stack_[num]]=0,num--;
        } num--; //记录所有的连通分量
    }
}

int main(/*hs_love_wjy*/){
    int x,y,ans=0; cin>>n>>p; 
    for(int i=1;i<=n;i++) a[i]=1e9;
    
    for(int i=1;i<=p;i++) reads(x),reads(y),a[x]=y;
    cin>>m; for(int i=1;i<=m;i++) reads(x),reads(y),add(x,y);

    for(int i=1;i<=n;i++) //需要缩点的情况
        if(!dfn[i]&&a[i]!=1e9) tarjan(i);

    for(int i=1;i<=n;i++) if(dfn[i]==0) //无法完全连通
      { cout<<"NO"<<endl<<i<<endl; return 0;}
    
    for(int x=1;x<=n;x++) //统计记录经过缩点之后,
        for(int i=head[x];i;i=e[i].nextt) //每个强连通分量的入度
            if(col[x]!=col[e[i].ver]) rd[col[e[i].ver]]++;
    
    for(int i=1;i<=sum;i++) if(!rd[i]) ans+=circle[i];
    
    cout<<"YES"<<endl<<ans<<endl; return 0;
}

【例题7】【p3043】牛联盟

  • 给出n个点m条边的图,现把点和边分组,
  • 每条边只能和相邻两点之一分在一组,问分组方案数。

【标签】结论(无向图连通块只可能是环或树) + 分类讨论 + 无向图缩点

【分析】因为是无向图,所以寻找到的连通块只可能是环或树(可以使用并查集,dfs,tarjan...)

如果是环,只有两种方案(顺时针+逆时针);如果不是环,相当于是n个点,n-1条边,有n种方案。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<set>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/*【p3043】牛联盟
给出n个点m条边的图,现把点和边分组,每条边只能和相邻两点之一分在一组,问分组方案数。*/

/*【分析】因为是无向图,所以寻找到的连通块只可能是环或树(可以使用并查集,dfs,tarjan...)
如果是环,只有两种方案(顺时针+逆时针);如果不是环,相当于是n个点,n-1条边,有n种方案。*/

void reads(ll &x){ //读入优化(正负整数)
    int fa=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fa; //正负号
}

const ll N=500019,mod=1000000007;

ll n,p,m,tot=0,head[N*2],col[N],x[N],y[N],circle[N],circle2[N];

ll dfn[N],low[N],vis[N],stack_[N],dfss=0,num=0,sum=0; 
//↑↑tarjan算法中的 四个数组 和 三个变量

struct node{ ll ver,nextt; }e[N*2];

void add(ll u,ll v){ e[++tot].ver=v,e[tot].nextt=head[u],head[u]=tot; }

void tarjan(ll u){ //dfss记录当前dfs序到达的数字
    
    dfn[u]=low[u]=++dfss,vis[u]=true,stack_[++num]=u; //步骤一:初始化
    
    for(ll i=head[u];i;i=e[i].nextt){ //步骤二:枚举连向点,递归更新
        if(!dfn[e[i].ver]) tarjan(e[i].ver),low[u]=min(low[u],low[e[i].ver]);
        else if(vis[e[i].ver]) low[u]=min(low[u],dfn[e[i].ver]); //这里写dfn或low都可以
    } //↑↑步骤三:已经到达过,判断是否在当前栈内(栈内都是当前情况下能相连的点)
    
    if(dfn[u]==low[u]){
        col[u]=++sum; vis[u]=0; circle[sum]=1;
        while(stack_[num]!=u){
            circle[sum]++,col[stack_[num]]=sum,
            vis[stack_[num]]=0,num--;
        } num--; //记录所有的连通分量 和 大小
    }
}

int main(/*hs_love_wjy*/){
    ll ans=1; cin>>n>>m; 
    
    for(ll i=1;i<=m;i++) reads(x[i]),reads(y[i]),
        add(x[i],y[i]),add(y[i],x[i]);

    for(ll i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i);

    for(ll i=1;i<=m;i++) //求无向图的连通块是树还是环
        if(col[x[i]]==col[y[i]]) circle2[col[x[i]]]++;
    
    for(ll i=1;i<=sum;i++){
        if(circle[i]==circle2[i]) circle[i]=2; //如果是环,只有两种方案
        ans=(ans*(circle[i]%mod))%mod; //如果不是环,相当于是n个点,n-1条边,有n种方案
    } printf("%lld\n",ans); return 0;
}

 

 

                                    ——时间划过风的轨迹,那个少年,还在等你

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值