zoukankan      html  css  js  c++  java
  • 强连通分量

    强连通分量

    目录

    • 基本概念

    • (Kosaraju)算法

    • (Tarjan)算法

    • 例题讲解

    • 题目推荐

    • 学习资源


    基本概念

    • 连通图

    无向图中,从任意点(i)可以到达任意点(j)

    • 强连通图

    有向图中,从任意点(i)可以到达任意点(j)

    • 弱连通图(了解即可)

    人为地将有向图看做无向图后,从任意点(i)可以到达任意点(j)

    • 极大强连通子图

    (G)是一个极大强连通子图,当且仅当(G)是一个强连通子图且不存在另一个强连通子图(G'),使得(G)(G')的真子集

    • 强连通分量

    有向非强连通图的极大强连通子图

    因为来现实生活中有意义的强连通图很少,所以一般讨论的都是强连通分量

    若将有向图中的强连通分量都缩为一个点,则原图就会变成一个DAG(有向无环图),如下图(1)-图(2)所示:

    图(1)

    图(2)

    来讲(啰嗦 )一下,因为强连通分量相当于环啊,将环缩为点之后那就是无环图咯,这个很好想,证明的话反证法即可

    • 强连通分量的应用
    1. 有向图的缩点:见上图示

    2. 解决(2-SAT)问题(还没学....之后更新啊qwq)


    (Kosaraju)算法

    基于两次(DFS)的有向图强连通分量算法,时间复杂度为(O(n+m))

    • 算法框架
    1. 对原图(G)进行(DFS),记录每个节点访问完的顺序(dfn[i])并将点压入

    2. 选择最晚访问完的节点对(G)反向图进行第二次(DFS),删除能够遍历到的节点,每次遍历到的一坨(或一个)节点构成一个强连通分量

    3. 一直执行((2))操作,直到所有节点都二次遍历完

    • 例子图示

    第一次(DFS)顺序:(3->2->1->4)!)

    第二次(DFS)顺序:(4,2->1,3) (一个逗号前为一个强连通分量)

    • 代码函数段

    个人认为比接下来的(Tarjan)好理解,但是使用的更多的还是扩展性更强的(Tarjan)

    inline void dfs1(int x) {
        vis[x]=1;
        for(register int i=1;i<=n;i++) {
            if(!vis[i]&&map[x][i]) dfs1(i);
            dfn[++t]=x;
        }
    }
    
    inline void dfs2(int x) {
        vis[x]=t;
        for(register int i=1;i<=n;i++) {
            if(!vis[x]&&map[i][x]) dfs2(i);
        }
    }
    
    inline void ko() {
        t=0;
        for(register int i=1;i<=n;i++) {
            if(!vis[i]) dfs1(i);
        }
        memset(vis,0,sizeof(vis));
        t=0;
        for(register int i=n;i>=1;i--) {
            if(!vis[dfn[i]]) {
                t++;
                dfs2(dfn[i]);
            }
        }
    }
    

    (Tarjan)算法

    基于一次(DFS)的算法,时间复杂度也是(O(n+m))

    (kosaraju)算法的(DFS)不同,(Tarjan)(DFS)更类似于树的后序遍历

    上图理解吧:


    图片截屏自我的学习视频,在文章最后会贴上,侵删!

    至于很多博客讲的四种边(树枝边、前向边、后向边、横叉边),我个人认为是没有必要掌握的,了解一下就可以了

    • 所需变量
    1. (dfn[i]):表示节点(i)的遍历顺序(同(Kosaraju)算法)

    2. (low[i]):表示节点(i)可回溯到的最早遍历时间(初始与(dfn[i])一致)

    3. (fir[i]=x):表示节点(i)和节点(x)同属于一个强连通分量

    4. (q[top]):手写栈qwq

    5. 以及一啪啦的变量(可麻烦了)

    • 算法框架

    设当前点为(x)

    1. 初始(dfn[x])=(low[x])=(++ti)(时间戳)

    2. 入栈当前点并标记为访问过

    3. 遍历与(x)相连的点,进行下一层的(DFS),然后更新(low[x])

    4. 遍历完后,如果当前(x)(dfn)==(low),则可以弹出一个强连通分量了

    可能有点抽象,如果不好理解,可以先跳到文末点击视频链接,里面讲得敲详细qwq

    • 代码实现

    以下代码是根据洛谷P3387 【模板】缩点 这道题编的,大家注意区分啊

    另外,下面这份代码涉及到的拓扑排序,有兴趣的可以看我的另一篇博客

    (PS:变量申请那块奇丑...轻喷)

    #include <bits/stdc++.h>
    using namespace std;
    int n,m,u,v,tot,ans,a[520010],in[520010],fir[520010],head[520010];
    int ti,top,num,q[520010],vis[520010],dis[520010],sum[520010],dfn[520010],low[520010];
    
    struct node {  
    	int to,net,fro;
    } e[520010],es[520010];
    
    inline void add(int u,int v) {
    	e[++tot].to=v;
    	e[tot].fro=u;
    	e[tot].net=head[u];
    	head[u]=tot;
    }
    
    inline void tarjan(int x) {
    	dfn[x]=low[x]=++ti;
    	q[++top]=x;
    	vis[x]=1;
    	for(register int i=head[x];i;i=e[i].net) {
    		int v=e[i].to;
    		if(!dfn[v]) {
    			tarjan(v);
    			low[x]=min(low[x],low[v]);
    		}
    		else {
    			if(vis[v]) low[x]=min(low[x],dfn[v]);
    		}
    	}
    	if(low[x]==dfn[x]) {
    		int v=q[top];
    		while(top) {
    			fir[v]=x;
    			vis[v]=0;
    			if(v==x) {
    				top--;
    				break;
    			}
    			a[x]+=a[v];
    			v=q[--top];
    		}
    	}
    }
    
    inline void topo() {  //拓扑排序求最长路
    	queue<int> q;
    	for(register int i=1;i<=n;i++) {
    		dis[i]=a[i];
    		if(!in[i]&&fir[i]==i) q.push(i);
    	}
    	while(!q.empty()) {
    		int x=q.front();
    		q.pop();
    		for(register int i=head[x];i;i=es[i].net) {
    			int v=es[i].to;
    			dis[v]=max(dis[v],dis[x]+a[v]);
    			if(--in[v]==0) q.push(v);
    		}
    	}
    }
    
    int main() {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
    	for(register int i=1;i<=m;i++) {
    		scanf("%d%d",&u,&v);
    		add(u,v);
    	}
    	for(register int i=1;i<=n;i++) {
    		if(!dfn[i]) tarjan(i);
    	}
    	tot=0;
    	memset(vis,0,sizeof(vis)); 
    	memset(head,0,sizeof(head));
    	for(register int i=1;i<=m;i++) {
    		u=fir[e[i].fro];
    		v=fir[e[i].to];
    		if(u!=v) {
    			in[v]++;
    			es[++tot].to=v;
    			es[tot].net=head[u];
    			es[tot].fro=u;
    			head[u]=tot;
    		}
    	}
    	topo();
    	for(register int i=1;i<=n;i++) ans=max(ans,dis[i]);
    	printf("%d",ans);
    	return 0;
    }
    

    例题讲解

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

    个人认为很模板的题,思路稍微转换一下就出来了,写个小题解当做例题讲解qwq

    • 分析

    奶牛们之间的喜爱是单向的、可传递的,那么将文字描述抽象一下,即:

    奶牛们是点,喜爱关系是单向边,整个关系则构成了有向图

    相互喜爱的一群牛组成一个集合,则(N)头牛可以划分为(S)个集合(缩点的思想,缩环为点

    集合与集合之间也存在喜爱关系,则原图就转化为了DAG(有向无环图)

    这个时候我们就需要思考一下:到底什么样的牛是所有牛喜欢的那个?

    显然:是出度为(0)的集合中的牛

    为什么?因为出度为(0)则说明这个集合不喜爱其他的牛,则满足所有牛都喜欢这头牛(有点绕,画一下图会好一点,作者懒不想画了QAQ)

    由此,我们就将问题转换为了:缩点,然后求出度为(0)的点

    现在给出以上思路的(AC)程序:

    #include <bits/stdc++.h>
    using namespace std;
    int n,m,u,v,tot,num,out[520010],sum[520010],head[520010];
    int ti,top,q[520010],val[520010],fir[520010],vis[520010],dfn[520010],low[520010];
    
    struct node {
    	int to,net,fro;
    } e[520010];
    
    inline void add(int u,int v) {
    	e[++tot].to=v;
    	e[tot].fro=u;
    	e[tot].net=head[u];
    	head[u]=tot;
    }
    
    inline void tarjan(int x) {
    	dfn[x]=low[x]=++ti;
    	q[++top]=x;
    	vis[x]=1;
    	for(register int i=head[x];i;i=e[i].net) {
    		int v=e[i].to;
    		if(!dfn[v]) {
    			tarjan(v);
    			low[x]=min(low[x],low[v]);
    		}
    		else {
    			if(vis[v]) low[x]=min(low[x],dfn[v]);
    		}
    	}
    	if(low[x]==dfn[x]) {
    		int v=q[top];
    		while(top) {
    			fir[v]=x;
    			vis[v]=0;
    			if(v==x) {
    				top--;
    				break;
    			}
    			val[x]+=val[v];
    			v=q[--top];
    		}
    	}
    }
    
    int main() {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=n;i++) val[i]=1;
    	for(register int i=1;i<=m;i++) {
    		scanf("%d%d",&u,&v);
    		add(u,v);
    	}
    	for(register int i=1;i<=n;i++) {
    		if(!dfn[i]) tarjan(i);
    	}
    	for(register int i=1;i<=m;i++) {
    		u=fir[e[i].fro];
    		v=fir[e[i].to];
    		if(u!=v) out[u]++;
    	}
    	for(register int i=1;i<=n;i++) {
    		if(!out[i]&&fir[i]==i) {
    			sum[++num]=val[i];
    		}
    	}
    	if(num==1) printf("%d",sum[num]);
    	else if(num>=2) puts("0");
    	return 0;
    }
    

    PS:以下内容为我的无脑暴力骗分代码,可以跳过(52(pts)真香)

    #include <bits/stdc++.h>
    using namespace std;
    int n,m,u,v,sum,flag;
    int in[520010],out[520010];
    
    int main() {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=m;i++) {
    		scanf("%d%d",&u,&v);
    		out[u]++;
    		in[v]++;
    	}
    	for(register int i=1;i<=n;i++) {
    		if(out[i]==0) sum++;
    	}
    	if(sum==1) printf("1");
    	else if(sum>=2) printf("0");
    	else if(sum==0){
    		for(register int i=1;i<=n;i++) {
    			if(out[i]!=in[i]) {
    				flag=true;
    				break;
    			}
    		}
    		if(flag==false) printf("%d",n);
    	}
    	return 0;
    }
    

    例题讲解(正文)完毕~~


    题目推荐

    1. 洛谷P3387 【模板】缩点

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

    3. 洛谷P4782 【模板】2-SAT问题

    4. 洛谷P1407 [国家集训队]稳定婚姻


    学习资料

    1. (B)站学习视频,讲得真的很清晰,适合初学者

    2. (Tarjand)算法,图示详细但有点丑


  • 相关阅读:
    Unity3D-光照贴图技术
    登岳麓山
    第一个OC程序
    Unity3D之碰撞体,刚体
    TypeError: 'stepUp' called on an object that does not implement interface HTMLInputElement
    QQ互联登录回调路径错误redirect uri is illegal(100010)
    Quartz.Net使用
    C# 文件相关操作
    微信扫码支付模式一和模式二的区别
    ankhSVN安装后,VS2010使用
  • 原文地址:https://www.cnblogs.com/Eleven-Qian-Shan/p/13279783.html
Copyright © 2011-2022 走看看