tarjan求强连通份量+缩点+割点/割桥(点双/边双)以及一些证实

“tarjan陪伴强联通份量ios

生成树完成后思路才闪光数组

欧拉跑过的七桥古塘学习

让你 心驰神往”----《膜你抄》spa

 

自从听完这首歌,我就对tarjan开始心驰神往了,不过因为以前水平不足,一直没有时间学习。这两天好不容易学会了,写篇博客,也算记录一下。3d

 

1、tarjan求强连通份量code

一、什么是强连通份量?component

引用来自度娘的一句话:blog

“有向图强连通份量:在有向图G中,若是两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。若是有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通份量(strongly connected components)。”博客

一脸懵逼......不过倒也不难理解。string

反正就是在图中找到一个最大的图,使这个图中每一个两点都可以互相到达。这个最大的图称为强连通份量,同时一个点也属于强连通份量。

如图中强连通份量有三个:1-2-3,4,5

 

二、强连通份量怎么找?

噫......固然,经过肉眼能够很直观地看出1-2-3是一组强连通份量,但很遗憾,机器并无眼睛,因此该怎么判断强连通份量呢?

若是还是上面那张图,咱们对它进行dfs遍历。

能够注意到红边很是特别,由于若是按照遍历时间来分类的话,其余边都指向在本身以后被遍历到的点,而红边指向的则是比本身先被遍历到的点。

 

若是存在这么一条边,那么咱们能够yy一下,emmmm.......

从一个点出发,一直向下遍历,而后忽得找到一个点,那个点居然有条指回这一个点的边!

那么想必这个点可以从自身出发再回到自身

想必这个点和其余向下遍历的该路径上的全部点构成了一个环,

想必这个环上的全部点都是强联通的。

但只是强联通啊,咱们须要求的但是强连通份量啊......

 

那怎么办呢?

咱们仍是yy出那棵dfs树

不妨想一下,何时一个点和他的全部子孙节点中的一部分构成强连通份量

他的子孙再也没有指向他的祖先的边,却有指向他本身的边

由于只要他的子孙节点有指向祖先的边,显然能够构成一个更大的强联通图

 

好比说图中红色为强连通份量,而蓝色只是强联通图

 

那么咱们只须要知道这个点u下面的全部子节点有没有连着这个点的祖先就好了。

但彷佛还有一个问题啊......

 

咱们怎么知道这个点u它下面的全部子节点必定是都与他强联通的呢?

这彷佛是不对的,这个点u之下的全部点不必定都强联通

那么怎么在退回到这个点的时候,知道全部和这个点u构成强连通份量的点呢?

开个记录就好了

什么?!这么简单?

没错~就是这么简单~

若是在这个点以后被遍历到的点已经能与其下面的一部分点(也可能就只有他一个点)已经构成强连通份量,即它已是最大的

那么把它们一块儿从栈里弹出来就好了。

因此最后处理到点u时若是u的子孙没有指向其祖先的边,那么它以后的点确定都已经处理好了,一个常见的思想,能够理解一下。

因此就能够保证栈里留下来u后的点都是能与它构成强连通份量的。

 

彷佛作法已经明了了,用程序应该怎么实现呢?

 

因此为了实现上面的操做,咱们须要一些辅助数组

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

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

(3)、stack[ ],表示当前全部可能能构成是强连通份量的点。

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

那么按照之上的思路,咱们来考虑这几个数组的用处以及tarjan的过程。

 

假设如今开始遍历点u:

 

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

dfn能够理解,但为何low也要这么作呢?

 由于low的定义如上,也就是说若是没有子孙与u的祖先相连的话,dfn[u]必定是它和它的全部子孙中dfn最小的(由于它的全部子孙必定比他后搜到)

 

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

stack[ ]有什么用?

若是u在stack中,u以后的全部点在u被回溯到时u和栈中全部在它以后的点都构成强连通份量。

 

(3)、遍历u的每个能到的点,若是这个点dfn[ ]为0,即仍未访问过,那么就对点v进行dfs,而后low[u]=min{low[u],low[v]}

low[ ]有什么用?

应该能看出来吧,就是记录一个点它最大能连通到哪一个祖先节点(固然包括本身)

若是遍历到的这个点已经被遍历到了,那么看它当前有没有在stack[ ]里,若是有那么low[u]=min{low[u],low[v]}

若是已经被弹掉了,说明不管如何这个点也不能与u构成强连通份量,由于它不能到达u

若是还在栈里,说明这个点确定能到达u,一样u能到达他,他俩强联通

 

(4)、假设咱们已经dfs完了u的全部的子树那么以后不管咱们再怎么dfs,u点的low值已经不会再变了。

那么若是dfn[u]=low[u]这说明了什么呢?

再结合一下dfn和low的定义来看看吧

dfn表示u点被dfs到的时间,low表示u和u全部的子树所能到达的点中dfn最小的。

这说明了u点及u点之下的全部子节点没有边是指向u的祖先的了,即咱们以前说的u点与它的子孙节点构成了一个最大的强连通图即强连通份量

此时咱们获得了一个强连通份量,把全部的u点之后压入栈中的点和u点一并弹出,将它们的vis[ ]置为false,若有须要也能够给它们打上相同标记(同一个数字)

 

tarjan到此结束

至于手模?tan90°!网上有很多大佬已经手摸了很多样例了,想必不须要本蒟蒻再补充了。

 

结合上面四步代码已经能够写出了:

对了,tarjan一遍不能搜完全部的点,由于存在孤立点或者其余

因此咱们要对一趟跑下来尚未被访问到的点继续跑tarjan

怎么知道这个点有没有被访问呢?

看看它的dfn是否为0

这看起来彷佛是o(n^2)的复杂度,但其实均摊下来每一个点只会被遍历一遍

因此tarjan的复杂度为o(n)

 

来一道例题吧,这是模板题,应该作到提交框AC

[USACO06JAN]牛的舞会The Cow Prom

 给你n个点,m条边,求图中全部大小大于1的强连通份量的个数

输入样例#1:
5 4 2 4 3 5 1 2 4 1
输出样例#1: 
1



显然是tarjan水题,数出强连通份量的个数,给每一个强连通份量的点染色,统计出每一个强连通份量中点的个数,若是大于一,则答案加一。

代码:

#include<queue> #include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define inf 0x3f3f3f3f vector<int> g[10010]; int color[10010],dfn[20020],low[20020],stack[20020],vis[10010],cnt[10010]; int deep,top,n,m,sum,ans; void tarjan(int u) { dfn[u]=++deep; low[u]=deep; vis[u]=1; stack[++top]=u; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else { if(vis[v]) { low[u]=min(low[u],low[v]); } } } if(dfn[u]==low[u]) { color[u]=++sum; vis[u]=0; while(stack[top]!=u) { color[stack[top]]=sum; vis[stack[top--]]=0; } top--; } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); } for(int i=1;i<=n;i++) { if(!dfn[i]) { tarjan(i); } } for(int i=1;i<=n;i++) { cnt[color[i]]++; } for(int i=1;i<=sum;i++) { if(cnt[i]>1) { ans++; } } printf("%d\n",ans); }

 

2、tarjan缩点

其实这也是利用了tarjan求强连通份量的方法,对于一些贡献具备传导性,好比友情啊、路径上的权值啊等等。

思想就是由于强连通份量中的每两个点都是强连通的,能够将一个强连通份量当作一个超级点,而点权按题意来定。

 

来看一道题吧。

poj2186 Popular Cows

告诉你有n头牛,m个崇拜关系,而且崇拜具备传递性,若是a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被全部牛崇拜。

 

Sample Input
3 3
1 2
2 1
2 3
Sample Output
1

 

显然一个强联通份量内的全部点都是知足条件的,咱们能够对整张图进行缩点,而后就简单了。

剩下的全部点都不是强连通的,如今整张图就是一个DAG(有向无环图)

那么就变成一道水题了,由于这是一个有向无环图,不存在全部点的出度都不为零的状况。

因此必然有1个及以上的点出度为零,若是有两个点出度为零,那么这两个点确定是不相连的,即这两圈牛不是互相崇拜的,因而此时答案为零,若是有1个点出度为0,那么这个点就是被全体牛崇拜的,

这个点多是一个强联通份量缩成的超级点,因此应该输出整个强联通份量中点的个数。

代码:

#include<cmath> #include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; int dfn[10010],low[10010],vis[10010],stack[10010],color[10010],du[10010],cnt[10010]; int n,m,top,sum,deep,tmp,ans; vector<int> g[10010]; void tarjan(int u) { dfn[u]=low[u]=++deep; vis[u]=1; stack[++top]=u; int sz=g[u].size(); for(int i=0; i<sz; i++) { int v=g[u][i]; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else { if(vis[v]) { low[u]=min(low[u],low[v]); } } } if(dfn[u]==low[u]) { color[u]=++sum; vis[u]=0; while(stack[top]!=u) { color[stack[top]]=sum; vis[stack[top--]]=0; } top--; } } int main() { while(scanf("%d%d",&n,&m)!=EOF) { memset(vis,0,sizeof(du)); memset(vis,0,sizeof(low)); memset(dfn,0,sizeof(dfn)); memset(vis,0,sizeof(vis)); memset(vis,0,sizeof(cnt)); memset(vis,0,sizeof(color)); memset(vis,0,sizeof(stack)); for(int i=1; i<=n; i++) { g[i].clear(); } for(int i=1; i<=m; i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); } for(int i=1; i<=n; i++) { if(!dfn[i]) { tarjan(i); } } for(int i=1; i<=n; i++) { int sz=g[i].size(); for(int j=0; j<sz; j++) { int v=g[i][j]; if(color[v]!=color[i]) { du[color[i]]++; } } cnt[color[i]]++; } for(int i=1; i<=sum; i++) { if(du[i]==0) { tmp++; ans=cnt[i]; } } if(tmp==0) { printf("0\n"); } else { if(tmp>1) { printf("0\n"); } else { printf("%d\n",ans); } } } }

3、tarjan求割点、桥

一、什么是割点、桥

再来引用一遍度娘:

在一个无向图中,若是有一个顶点集合,删除这个顶点集合以及这个集合中全部顶点相关联的边之后,图的连通份量增多,就称这个点集为割点集合。

又是一脸懵逼。。。。

总而言之,就是这个点维持着双联通的继续,去掉这个点,这个连通份量就没法在维持下去,分红好几个连通份量。

好比说上图红色的即为一个割点。

桥:

若是一个无向连通图的边连通度大于1,则称该图是边双连通的 (edge biconnected),简 称双连通或重连通。一个图有桥,当且仅当这个图的边连通度为 1,则割边集合的惟一元素 被称为桥(bridge),又叫关节边(articulationedge)。一个图可能有多个桥。(该资料一样来自百度)

对于连通图有两种双联通,边双和点双,桥之于边双如同割点之于点双

如图则是一个桥。

二、割点和桥怎么求?

与以前强连通份量中的tarjan差很少。但要加一个特判,根节点若是有两个及以上的儿子,那么他也是割点。

 

 

模板题:洛谷3388

求割点的个数和数量

代码:

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
#define hi printf("hi!");
using namespace std; vector<int> g[10010]; int dfn[10010],low[10010],iscut[10010],son[10010]; int deep,root,n,m,ans; int tarjan(int u,int fa) { int child=0,lowu; lowu=dfn[u]=++deep; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { child++; int lowv=tarjan(v,u); lowu=min(lowu,lowv); if(lowv>dfn[u]) { iscut[u]=1; } } else { if(v!=fa&&dfn[v]<dfn[u]) { lowu=min(lowu,dfn[v]); } } } if(fa<0&&child==1) { iscut[u]=false; } low[u]=lowu; return lowu; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } for(int i=1;i<=n;i++) { if(!dfn[i]) { root=i; tarjan(i,-1); } } for(int i=1;i<=n;i++) { if(iscut[i]) { ans++; } } printf("%d\n",ans); for(int i=1;i<=n;i++) { if(iscut[i]) { printf("%d ",i); } } }

桥的求法也差很少

 

 

并无找到模板题目,因此只好把没检验过的代码放着了......若有错误还请留言指正

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
#define hi printf("hi!");
using namespace std; vector<pair<int,int> >bridge; vector<int> g[10010]; int dfn[10010],low[10010]; int deep,root,n,m,ans; int tarjan(int u,int fa) { int lowu; lowu=dfn[u]=++deep; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { int lowv=tarjan(v,u); lowu=min(lowu,lowv); if(lowv>dfn[u]) { int from,to; from=u; to=v; if(from>to) { swap(from,to); } bridge.push_back(make_pair(from,to)); } } else { if(v!=fa&&dfn[v]<dfn[u]) { lowu=min(lowu,dfn[v]); } } } low[u]=lowu; return lowu; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } for(int i=1;i<=n;i++) { if(!dfn[i]) { root=i; tarjan(i,-1); } } for(int i=0;i<bridge.size();i++) { printf("%d %d\n",bridge[i].first,bridge[i].second); } }

 

 

 

 

おわり

相关文章
相关标签/搜索