tarjan算法(割点/割边/点连通分量/边连通分量/强连通分量)
tarjan算法是在dfs生成一颗dfs树的时候按照访问顺序的先后,为每个结点分配一个时间戳,然后再用low[u]表示结点能访问到的最小时间戳
以上的各种应用都是在此拓展而来的。
割点:如果一个图去掉某个点,使得图的连通分支数增加,那么这个点就是割点
某个点是割点,当且仅当这个点的后代没有连回自己祖先的边。即low[v] >= dfn[u] , v是u的后代
需要注意的是根结点的特判,因为根结点没有祖先,根结点是割点,当且仅当根结点有两个以上的儿子。
问题:重边对该算法有影响吗?没有影响。
需要注意的地方? 图至少有三个点以上, 否则需要注意一下。
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <algorithm> 5 #include <iostream> 6 #include <queue> 7 #include <stack> 8 #include <vector> 9 #include <map> 10 #include <set> 11 #include <string> 12 #include <math.h> 13 using namespace std; 14 typedef long long LL; 15 const int INF = 1<<30; 16 const int N = 1000 + 10; 17 vector<int> g[N]; 18 int dfs_clock,dfn[N],low[N]; 19 bool isCut[N]; 20 void init(int n) 21 { 22 dfs_clock = 0; 23 for(int i=0; i<=n; ++i) 24 dfn[i] = low[i] = isCut[i] = 0; 25 } 26 //重边对割点没有影响,且该算法对有向图同样适用 27 void tarjan(int u, int fa) 28 { 29 dfn[u] = low[u] = ++dfs_clock; 30 int child = 0; 31 for(int i=0; i<g[u].size(); ++i) 32 { 33 int v = g[u][i]; 34 if(v==fa) continue;//如果是树枝边的反向访问,则不能用来更新low[u] 35 child++; 36 if(dfn[v]==0) 37 tarjan(v,u); 38 low[u] = min(low[u],low[v]);//用树枝边,或者后向边来跟新low[u] 39 if(low[v] >= dfn[u]) 40 isCut[u] = true; 41 } 42 if(fa==-1 && child>=2) isCut[u] = true; 43 } 44 int main() 45 { 46 int n,m,i,u,v; 47 while(scanf("%d%d",&n,&m)!=EOF) 48 { 49 init(n); 50 for(i=0; i<m; ++i) 51 { 52 scanf("%d%d",&u,&v); 53 g[u].push_back(v); 54 g[v].push_back(u); 55 } 56 tarjan(1,-1); 57 for(i=1; i<=n; ++i) 58 if(isCut[i]) 59 printf("%d ",i); 60 puts(""); 61 } 62 return 0; 63 }
割边:如果一个图去掉某条边,使得图的连通分支数增加,那么这条边就是割边(桥)
某条边是割边,当且仅当某个点的后代没有连回自己或者自己祖先的边,即low[v] > dfn[u], 那么边(u,v)是割边
问题:重边对该算法有影响吗? 有影响。 所以要判断是不是有重边
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <algorithm> 5 #include <iostream> 6 #include <queue> 7 #include <stack> 8 #include <vector> 9 #include <map> 10 #include <set> 11 #include <string> 12 #include <math.h> 13 using namespace std; 14 typedef long long LL; 15 const int INF = 1<<30; 16 const int N = 1000 + 10; 17 vector<int> g[N]; 18 int dfs_clock,dfn[N],low[N],cntCut; 19 void init(int n) 20 { 21 cntCut = dfs_clock = 0; 22 for(int i=0; i<=n; ++i) 23 { 24 dfn[i] = low[i] = 0; 25 g[i].clear(); 26 } 27 } 28 //重边对割边有影响,比如有2--3 ,2--3两条边,那么边2--3就不是割边,因为去掉还是连通的,所以要判断一下 29 void tarjan(int u, int fa) 30 { 31 dfn[u] = low[u] = ++dfs_clock; 32 bool flag = false; 33 for(int i=0; i<g[u].size(); ++i) 34 { 35 int v = g[u][i]; 36 if(v==fa && !flag)//在这里判断有没有重边 37 { 38 flag = true; 39 continue; 40 } 41 if(dfn[v]==0) 42 tarjan(v,u); 43 low[u] = min(low[u],low[v]); 44 if(low[v] > dfn[u])//这里统计的是割边的数量,如果要记录割边,那么就标记边,或者把边入栈 45 cntCut++; 46 } 47 } 48 int main() 49 { 50 int n,m,i,u,v; 51 while(scanf("%d%d",&n,&m)!=EOF) 52 { 53 init(n); 54 for(i=0; i<m; ++i) 55 { 56 scanf("%d%d",&u,&v); 57 g[u].push_back(v); 58 g[v].push_back(u); 59 } 60 tarjan(1,-1); 61 printf("%d\n",cntCut); 62 } 63 return 0; 64 }
点-双连通分量:如果任意两点存在两条点不重复的路径,那么就说这个图是点-双连通的。点-双连通的极大子图称为双连通分量
双连通分量之间的分界点是割点。而且双连通分量不可能分布在树根结点两端。所以我们将边入栈,当遇到割点时,就将边出栈,直到有边等于当前边。就跳出
问题:重边对该算法有影响吗? 没有影响
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <algorithm> 5 #include <iostream> 6 #include <queue> 7 #include <stack> 8 #include <vector> 9 #include <map> 10 #include <set> 11 #include <string> 12 #include <math.h> 13 using namespace std; 14 typedef long long LL; 15 const int INF = 1<<30; 16 const int N = 1000 + 10; 17 struct Edge 18 { 19 int u,v; 20 Edge(){}; 21 Edge(int u, int v) 22 { 23 this->u = u; 24 this->v = v; 25 } 26 }; 27 vector<int> g[N],bcc[N]; 28 int bccno[N],dfn[N],low[N],dfs_clock,cnt; 29 stack<Edge> st; 30 void init(int n) 31 { 32 cnt = dfs_clock = 0; 33 for(int i=0; i<=n; ++i) 34 { 35 bccno[i] = low[i] = dfn[i] = 0; 36 bcc[i].clear(); 37 g[i].clear(); 38 } 39 } 40 void tarjan(int u, int fa) 41 { 42 dfn[u] = low[u] = ++dfs_clock; 43 for(int i=0; i<g[u].size(); ++i) 44 { 45 int v = g[u][i]; 46 if(dfn[v]==0) 47 { 48 st.push(Edge(u,v)); 49 tarjan(v,u); 50 low[u] = min(low[u],low[v]); 51 if(low[v] >= dfn[u])//如果这个点是割点,那么先前入栈的一些边是属于一个双连通分量的 52 { 53 Edge x; 54 cnt++; 55 for(;;) 56 { 57 x = st.top(); 58 st.pop(); 59 if(bccno[x.u] != cnt) 60 { 61 bccno[x.u] = cnt; 62 bcc[cnt].push_back(x.u); 63 } 64 if(bccno[x.v] != cnt) 65 { 66 bccno[x.v] = cnt; 67 bcc[cnt].push_back(x.v); 68 } 69 if(x.u==u && x.v==v) 70 break; 71 } 72 } 73 } 74 else if(v!=fa && dfn[v] < dfn[u]) 75 { 76 st.push(Edge(u,v)); 77 low[u] = min(low[u],dfn[v]); 78 } 79 80 } 81 } 82 int main() 83 { 84 int n,m,i,u,v; 85 while(scanf("%d%d",&n,&m)!=EOF) 86 { 87 init(n); 88 for(i=0; i<m; ++i) 89 { 90 scanf("%d%d",&u,&v); 91 g[u].push_back(v); 92 g[v].push_back(u); 93 } 94 tarjan(1,-1); 95 for(i=1; i<=cnt; ++i) 96 { 97 printf("bcc %d has vertex:",i); 98 while(!bcc[i].empty()) 99 { 100 printf("%d ",bcc[i].back()); 101 bcc[i].pop_back(); 102 } 103 puts(""); 104 } 105 } 106 return 0; 107 }
边-双连通分量:如果任意两点存在两条边不重复的路径,那么就说这个图是边-双连通的,边-双连通的极大子图成为边双连通分量。
边双连通分量的分界点是割边。双连通分量可以分布在根结点的两端。所以不能在for(int i=0; i<g[u].size(); ++i) 这个循环里面判断割边,而要在外面递归返回时判断
问题:重边对该算法有影响吗?有影响,就好像影响割边算法一样
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <algorithm> 5 #include <iostream> 6 #include <queue> 7 #include <stack> 8 #include <vector> 9 #include <map> 10 #include <set> 11 #include <string> 12 #include <math.h> 13 using namespace std; 14 typedef long long LL; 15 const int INF = 1<<30; 16 const int N = 1000 + 10; 17 vector<int> g[N]; 18 stack<int> st; 19 int dfn[N],low[N],dfs_clock,bccno[N],cnt; 20 void init(int n) 21 { 22 cnt = dfs_clock = 0; 23 for(int i=0; i<n; ++i) 24 { 25 dfn[i] = low[i] = 0; 26 bccno[i] = 0; 27 g[i].clear(); 28 } 29 } 30 31 void tarjan(int u, int fa)//边双连通分量可能分布在某子树树根的两个分支上 32 { 33 low[u] = dfn[u] = ++dfs_clock; 34 st.push(u); 35 bool flag = 0; 36 for(int i=0; i<g[u].size(); ++i) 37 { 38 int v = g[u][i]; 39 if(v==fa && !flag) //重边对边连通分量的判断有影响,所以要判断一下 40 { 41 flag = true; 42 continue; 43 } 44 if(dfn[v]==0) 45 tarjan(v,u); 46 low[u] = min(low[u],low[v]); 47 } 48 if(dfn[u]==low[u])//说明这个点的所有后代都没有连回自己祖先的边, 49 { 50 cnt++; 51 for(;;) 52 { 53 int x = st.top(); 54 st.pop(); 55 bccno[x] = cnt; 56 if(x==u) 57 break; 58 } 59 } 60 } 61 int main() 62 { 63 int n,m,i,u,v; 64 while(scanf("%d%d",&n,&m)!=EOF) 65 { 66 init(n); 67 for(i=0; i<m; ++i) 68 { 69 scanf("%d%d",&u,&v); 70 g[u].push_back(v); 71 g[v].push_back(u); 72 } 73 tarjan(1,-1); 74 for(i=1; i<=n; ++i) 75 printf("vexter %d belong to bcc %d\n",i,bccno[i]); 76 } 77 return 0; 78 }
问添加最少多少条边,使得整个图边-双连通, 首先求出边-双连通分量,然后缩点,最后变成一棵树,那么要加的边 就是(叶子结点+1)/2
强连通分量:如果有向图的任意两点可以,那么就说这个图是强连通的,一个图的极大子图是强连通的,那么就说这个子图是强连通分量
对于一个scc,我们要判断哪个点是该scc最先被发现的点,然后将后来发现的点出栈,知道遇到这个点。 那么出栈的点都属于一个强连通分量
问题:重边对该算法有影响吗?没有影响
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <algorithm> 5 #include <iostream> 6 #include <queue> 7 #include <stack> 8 #include <vector> 9 #include <map> 10 #include <set> 11 #include <string> 12 #include <math.h> 13 using namespace std; 14 typedef long long LL; 15 const int INF = 1<<30; 16 const int N = 1000 + 10; 17 vector<int> g[N]; 18 stack<int> st; 19 int cnt,dfs_clock,dfn[N],low[N],sccno[N]; 20 void init(int n) 21 { 22 cnt = dfs_clock = 0; 23 for(int i=0; i<=n; ++i) 24 { 25 dfn[i] = low[i] = sccno[i] = 0; 26 g[i].clear(); 27 } 28 } 29 30 void tarjan(int u, int fa) 31 { 32 dfn[u] = low[u] = ++dfs_clock; 33 st.push(u); 34 for(int i=0; i<g[u].size(); ++i) 35 { 36 int v = g[u][i]; 37 if(dfn[v]==0) 38 { 39 tarjan(v,u); 40 low[u] = min(low[u],low[v]); 41 } 42 else if(sccno[v]==0)//因为有向图存在横插边,不能用横插边来更新low[u] 43 { 44 low[u] = min(low[u],low[v]); 45 } 46 } 47 //同样,因为强连通分量可以分布在根结点的两个分支上,所以在递归返回的时候调用 48 if(low[u] == dfn[u]) 49 { 50 cnt++; 51 for(;;) 52 { 53 int x = st.top(); 54 st.pop(); 55 sccno[x] = cnt; 56 if(x==u) 57 break; 58 } 59 } 60 } 61 int main() 62 { 63 int n,m,i,u,v; 64 while(scanf("%d%d",&n,&m)!=EOF) 65 { 66 init(n); 67 for(i=0; i<m; ++i) 68 { 69 scanf("%d%d",&u,&v); 70 g[u].push_back(v); 71 } 72 tarjan(1,-1); 73 for(i=1; i<=n; ++i) 74 printf("vertex %d belong to scc %d\n",i,sccno[i]); 75 } 76 return 0; 77 }