图的连通性专题及模板
图的连通性
dfn[u]: 表示节点u的搜索优先级 low[u]: 表示节点u,通过其本身或其子节点能到达的最小有搜索优先级 low[u] = Min{ 1. dfn[u] 其本身搜索优先级 2. Min{ low[v] } 其子节点v能到达的最小优先级 3. Min( dfn[v] ) 其通过回边(u,v),其中v为u的祖先节点,的优先级 }
一 无向图
1. 割点
又名关键点,若删除该点与其发出的边.则整个图不连通.
当前顶点u是一个关键点的充要条件是:
1. 若顶点U是根,则其必定包含两个以上的子节点. (因为若只有一个.删除了U之后,图仍然连通)
2. 若顶点U不是根, 则当 dfn[u] <= low[v] , (其中V是U的子孙节点). 因为V无法通过其本身或子孙到达U或者U更高级的点.所以删除U后,图不连通.
poj 1523 SPF
#include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> using namespace std; const int N = 1010; int edge[N][N]; int n, son; int subnet[N], dfn[N], low[N], tmpdfn; bool vis[N]; void init(){ tmpdfn = 1; son = 0; memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(vis,0,sizeof(vis)); memset(subnet,0,sizeof(subnet)); low[1] = dfn[1] = 1; vis[1] = true; } void dfs(int u){ // printf("u = %d\n", u ); for(int v = 1; v <= n; v++) { if( edge[u][v] ){ if( !vis[v] ){ vis[v] = true; dfn[v] = low[v] = ++tmpdfn; dfs( v ); low[u] = min( low[u], low[v] ); if( low[v] >= dfn[u] ){ if( u != 1 ) subnet[u]++; else son++; } } else low[u] = min( low[u], dfn[v] ); } } } int main(){ // freopen("1.in","r",stdin); int Case = 0; while( 1 ){ int u, v; n = 0; scanf("%d", &u); if( u == 0 ) break; memset(edge,0,sizeof(edge)); scanf("%d",&v); if( u > n ) n = u; if( v > n ) n = v; edge[u][v] = edge[v][u] = 1; while(1){ scanf("%d",&u); if( u == 0 ) break; scanf("%d",&v); if(u > n) n = u; if(v > n) n = v; edge[u][v] = edge[v][u] = 1; } if( Case ) puts(""); printf("Network #%d\n", ++Case); init(); dfs(1); if( son > 1 ) subnet[1] = son-1; bool find = false; for(int i = 1; i <= n; i++) { if( subnet[i] ){ find = true; printf(" SPF node %d leaves %d subnets\n", i, subnet[i]+1); } } if( !find ) printf(" No SPF nodes\n"); } return 0; }
2. 割边(桥)
割边的判定条件: dfn[u] < low[v] . 割边要注意重边
zoj 2588 burning bridge
#include<cstdio> #include<cstring> #include<cstdlib> #include<map> #include<vector> #include<algorithm> using namespace std; const int N = (int)1e4+10; struct Edge{ int v, tag, nxt; }edge[N*20]; int head[N], idx; int n, m; int dfn[N], low[N], dep; bool vis[N]; map< pair<int,int>,int > mp; vector<int> S; void AddEdge(int u,int v) { for(int i = head[u]; ~i; i = edge[i].nxt ) if( edge[i].v == v ){ edge[i].tag++; return; } edge[idx].v = v; edge[idx].nxt = head[u]; edge[idx].tag = 0; head[u] = idx++; } void input(){ memset( head, -1, sizeof(head)); idx = 0; scanf("%d%d", &n,&m); mp.clear(); for(int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u,&v); mp[ make_pair(u,v) ] = mp[ make_pair(v,u) ] = i+1; AddEdge(u,v), AddEdge(v,u); } } void tarjan(int u,int pre) { vis[u] = true; dfn[u] = low[u] = ++dep; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( v == pre ) continue; if( !vis[v] ) { tarjan( v, u ); low[u] = min( low[u], low[v] ); } else low[u] = min( low[u], dfn[v] ); if( (low[v] > dfn[u]) && !edge[i].tag ) S.push_back( mp[make_pair(u,v)] ); } } void solve(){ S.clear(); memset(vis,0,sizeof(vis)); dep = 0; tarjan(1,0); sort(S.begin(), S.end()); int tot = (int)S.size(); printf("%d\n", tot); for(int i = 0; i < tot; i++) printf(i == 0 ? "%d" :" %d", S[i] ); if(tot) puts(""); } int main(){ /// freopen("1.in","r",stdin); int _; scanf("%d", &_); for(int Case = 0; Case < _; Case++) { if(Case) puts(""); input(); solve(); } return 0; }
3. 重连通分量(顶点重复,关键点)
具体做法是,通过一个栈将所有经过的边存储起来.当顶点U的子节点满足 dfn[u] <= low[v] 时,证明顶点U为一个关键点.则顺带找出了一个重连通分量.
要注意的是, 找的过程.所有的边只能被访问一次. 注意标记.
#include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<stack> using namespace std; const int N = 10101; int n, m; struct Edge{ int u, v, nxt; Edge(){} Edge(int _u,int _v){ u = _u; v = _v; nxt = 0; } int cmp( Edge &t ){ return (u==t.u&&v==t.v)||(u==t.v&&v==t.u); } void print(){ printf("%d->%d ", u, v ); } }edge[N<<4]; int head[N], idx; int dfn[N], low[N], dep; bool vis[N]; bool gao[N][N]; stack< Edge > S; void AddEdge(int u,int v) { edge[idx].u = u; edge[idx].v = v; edge[idx].nxt = head[u]; head[u] = idx++; } void input(){ memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < m; i++) { int u , v; scanf("%d%d", &u,&v); AddEdge(u,v); AddEdge(v,u); } } void tarjan(int u,int pre) { dfn[u] = low[u] = ++dep; vis[u] = true; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( v == pre || gao[v][u] || gao[u][v] ) continue; gao[v][u] = gao[u][v] = true; // 这里要注意标记边.回边也只能走一次. S.push( Edge(u,v) ); if( !vis[v] ){ tarjan(v,u); low[u] = min( low[u], low[v] ); } else low[u] = min( low[u], dfn[v] ); // 回边 if( low[v] >= dfn[u] ){ //删除顶点U,子节点V所在的子树将脱离. Edge t = Edge(u,v); while(1) { Edge t1 = S.top(); S.pop(); t1.print(); if( t1.cmp( t ) ) break; } puts(""); } } } void solve(){ memset( vis, 0, sizeof(vis)); memset( gao, 0, sizeof(gao)); dep = 0; tarjan(1,0); } int main() { freopen("1.txt","r",stdin); //while( scanf("%d%d", &n,&m) != EOF) scanf("%d%d", &n,&m); { input(); solve(); } return 0; } /* 7 9 1 2 1 3 1 6 1 7 2 3 2 4 2 5 4 5 6 7 */
4. 边双连通分量
poj 3177 Redundant Paths
题意是使所有的顶点间都有两条路可达, 路径可共顶点但不可共边.
做法是. 将同一个边双连通分量的所有点收缩成一个点. 新构成的顶点形成一棵树.
此时的任意两个顶点间都需要有至少两条相互"独立边", 则可以让成对的叶子节点连边.
所有总方案数 = (叶子节点数量+1)/2, 单个叶子节点也要连到其它节点去.
#include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> using namespace std; const int N = 1010; struct Node{ int v, nxt; }edge[N*100]; int head[N], idx; int n, m; int st[N]; int belong[N], dfn[N], low[N]; bool vis[N]; int bridge[N*10][2], nbridge, D[N]; int find(int x){ return x==st[x]?x:(st[x]=find(st[x]));} void Union(int a,int b){ int x = find(a), y = find(b); if( x != y ) st[x] = y; } void AddEdge(int u,int v) { edge[idx].v = v; edge[idx].nxt = head[u]; head[u] = idx++; } void input(){ memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u,&v); u--, v--; AddEdge(u,v), AddEdge(v,u); } } void tarjan(int u,int pre,int dep){ vis[u] = true; dfn[u] = low[u] = dep; for(int i = head[u]; ~i; i = edge[i].nxt) { int v = edge[i].v; if( v == pre ) continue; if( vis[v] ) low[u] = min( low[u], dfn[v] ); else{ tarjan( v,u,dep+1); low[u] = min( low[u], low[v] ); if( dfn[u] >= low[v] ) Union(u,v); if( dfn[u] < low[v] ){ bridge[nbridge][0] = u; bridge[nbridge++][1] = v; } } } } int GetConnection(){ for(int i = 0; i < n; i++) st[i] = i; nbridge = 0; memset(vis,0,sizeof(vis)); memset(belong,-1,sizeof(belong)); tarjan(0,-1,1); int conn = 0; for(int i = 0; i < n; i++) { int k = find(i); if( belong[k] == -1 ) belong[k] = conn++; belong[i] = belong[k]; } return conn; } void solve(){ int w = GetConnection(); memset( D, 0, sizeof(D)); for(int i = 0; i < nbridge; i++) { int u = bridge[i][0], v = bridge[i][1]; D[ belong[u] ]++, D[ belong[v] ]++; } int cnt = 0; for(int i = 0; i < w; i++) if( D[i] == 1 ) cnt++; printf("%d\n", (cnt+1)/2 ); } int main(){ while( scanf("%d%d", &n,&m) != EOF) { input(); solve(); } return 0; }
5. 点连通度
6. 边连通度
7. 无向图强连通分量缩点建树
hdu 4612 warm up
题目算法很好想. 添加一条边使"割边"最小. 则将 所有的强连通分量缩成一个点.然后构成一棵树.之后求出树上最长路,
这时候将最长路连起来,剩下的桥固然最小.
无向图强连通分量求法同 有向差不多. 可以选择 询问完所有子节点后利用 dfn[u]==low[u] 压栈合并. 也可以在里头用 low[v] <= dfn[u]
条件判定后用 并查集合并.
不管有向图,还是无向图. 要特别注意重边. 求割边主要是否要标记边. 这里有重边.就可以将两个顶点合并成强连通分量.
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<stack> #include<algorithm> using namespace std; #pragma comment(linker, "/STACK:1024000000,1024000000") const int N = 200010; const int M = 1000010; struct Edge{ int v, nxt, flag; }edge[M<<2]; int head[N], idx; int n, m; int dfn[N], low[N], dep, vis[N]; int id[N], conn; bool instack[N]; int nbridge, bridge[M][2]; int maxlen; stack<int> st; void AddEdge(int u,int v){ edge[idx].v = v; edge[idx].nxt = head[u]; edge[idx].flag = 1; head[u] = idx++; edge[idx].v = u; edge[idx].nxt = head[v]; edge[idx].flag = 1; head[v] = idx++; } void input(){ memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u,&v); AddEdge(u,v); } } void tarjan(int u,int pre) // 无向图,数据有回边.需要将其看做不同边.且边需要标记... { vis[u] = true; dfn[u] = low[u] = ++dep; st.push(u); instack[u] = true; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( edge[i].flag == false ) continue; edge[i].flag = edge[i^1].flag = false; if( !vis[v] ){ tarjan(v,u); low[u] = min( low[u], low[v] ); if( (dfn[u] < low[v]) ){ bridge[ nbridge ][0] = u; bridge[ nbridge++ ][1] = v; } } else if( instack[v] ) low[u] = min( low[u], dfn[v] ); } if( dfn[u] == low[u] ) { int t; do{ id[t=st.top()] = conn; st.pop(); instack[t] = false; }while(t!=u); conn++; } } void GetConnect(){ memset(vis,0,sizeof(vis)); memset(instack,0,sizeof(instack)); nbridge = 0; conn = 0; dep = 0; while( !st.empty() ) st.pop(); for(int i = 1; i <= n; i++) if( !vis[i] ) tarjan(i,0); // Debug /* for(int i = 1; i <= n; i++) printf("dfn[%d] = %d, low[%d] = %d\n", i,dfn[i], i,low[i]); for(int i = 1; i <= n; i++) printf("id[%d] = %d\n", i, id[i] );*/ } void ReGraph(){ memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < nbridge; i++){ int u = id[ bridge[i][0] ], v = id[ bridge[i][1] ]; // printf("u = %d, v = %d\n", u, v ); AddEdge(u,v); } } int dfs(int u,int pre) { int tmp = 0; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( v == pre ) continue; int d = dfs(v,u); maxlen = max( maxlen, tmp+d ); tmp = max(tmp,d); } return tmp+1; } void solve(){ GetConnect(); ReGraph(); maxlen = 0; dfs(0,-1); // printf("maxlen = %d, conn = %d\n", maxlen, conn); printf("%d\n", conn-(maxlen+1) ); } int main(){ while( scanf("%d%d", &n,&m) != EOF ) { if(n + m == 0) break; input(); solve(); } return 0; }
二 有向图
1. 有向图强连通分量
当 dfn[u] == dfn[v]时,以u为根的搜索子树上所有结点是一个强连通分量.
Tarjan( u ) { dfn[u] = low[u] = ++tmpdfn; Stack.push( u ); for each (u,v) in E { if( v is not visit ) // 此时当前边是生成树的边 { tarjan( v ); low[u] = min( low[u], low[v] ) } else if( v is in stack ) // 此时为回边 low[u] = min( low[u], dfn[v] ) // else 此时为 交叉边,其他连同分量上的边. } if( dfn[u] == low[u] ) repeat v = stack.pop; 将v腿栈,为该连通分量的一个顶点 print v until( u == v ) }
poj 2676 Going from u to v or from v to u?
题目大意是给一个有向图,让判定该图是否任意两点可达.( u -> v or v->u )
算法是, 先求出所有强连通分量,然后进行缩点.构成新图. 然后对新图做拓扑排序, 若拓排后的序列相邻间都有边相连.则符合要求.
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<algorithm> #include<stack> #include<queue> using namespace std; #pragma comment(linker, "/STACK:1024000000,1024000000") const int N = 1010; int n, m; struct Node{ int v, nxt; }edge[N*100]; int head[N], idx; int dfn[N], low[N], id[N], dep; bool vis[N], instack[N]; int graph[N][N]; int D[N], topo[N]; int conn; stack<int> st; void AddEdge(int u,int v) { edge[idx].v = v; edge[idx].nxt = head[u]; head[u] = idx++; } void input(){ memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u,&v); AddEdge(u,v); } } void tarjan(int u,int pre) { st.push(u); instack[u] = true; vis[u] = true; dfn[u] = low[u] = ++dep; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; // if( v == pre ) continue; if( !vis[v] ){ // 生成树的边. tarjan(v,u); low[u] = min( low[u], low[v] ); } else if( instack[v] ) // 在栈中.回边. low[u] = min( low[u], dfn[v] ); } if( dfn[u] == low[u] ){ //顶点u为根的子树是一个强连同块 int t; do{ id[ t=st.top() ] = conn; st.pop(); instack[t] = false; //low[t] = n; }while( t != u ); conn++; //强连通分量增加 } } int TopSort(){ memset( D, 0, sizeof(D)); memset( vis, 0, sizeof(vis)); while( !st.empty() ) st.pop(); for(int i = 0; i < conn; i++){ for(int j = 0; j < conn; j++) if(i != j) D[i] += graph[j][i]; if( D[i] == 0 ) st.push(i), vis[i] = true; } // if( st.size() > 1 ) return 0; int tot = 0; while( !st.empty() ) { int u = st.top(); st.pop(); vis[u] = false; topo[tot++] = u; //printf("TopSort: u = %d\n", u ); for(int v = 0; v < conn; v++) if( graph[u][v] ) { if(u == v) continue; D[v] -= graph[u][v]; if( D[v] == 0 && !vis[v] ) st.push(v), vis[v] = true; } } if( tot < conn ) return 0; return 1; } void solve(){ conn = 0; dep = 0; while( !st.empty() ) st.pop(); memset(vis,0,sizeof(vis)); memset(instack,0,sizeof(instack)); for(int i = 1; i <= n; i++) if( !vis[i] ) tarjan(i,0); memset( graph, 0, sizeof(graph)); for(int u = 1; u <= n; u++) { for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; graph[ id[u] ][ id[v] ] = 1; } } if( !TopSort() ) puts("No"); else { //for(int i = 0; i < conn; i++) // printf("%d ", topo[i] ); puts(""); bool flag = true; for(int i = 0; i < conn-1; i++) if( graph[ topo[i] ][ topo[i+1] ] == 0 ) flag = false; puts( flag ? "Yes" : "No"); } } int main(){ freopen("1.txt","r",stdin); int T; scanf("%d", &T); while( T-- ) { scanf("%d%d", &n,&m); input(); solve(); } return 0; }
poj 2186 Popular Cows
可以推出,若强连通分量中的一头牛A, 仰慕强连通分量外的一头牛B, 则连同分量内的其他牛都会仰慕牛B.
所以我们可以将 每个强连通分量 收缩成一个顶点. 然后因为要求 受所有牛仰慕的牛的数量. 所以这个顶点.
要么是一个 强连通缩点,要么就是单独一头牛. 此时应只有当前一个顶点 出度为0. 否则矛盾.
#include<cstdio> #include<cstring> #include<cstdlib> #include<stack> #include<algorithm> using namespace std; const int N = 10010; struct Edge{ int v, nxt; }edge[N*10]; int head[N], idx; int n, m; int dfn[N], low[N], dep; int id[N], conn, num[N]; bool vis[N], instack[N]; int D[N]; stack<int> st; void AddEdge(int u,int v) { edge[idx].v = v; edge[idx].nxt = head[u]; head[u] = idx++; } void input() { memset(head,-1,sizeof(head)); idx = 0; for(int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u,&v); AddEdge(u,v); } } void tarjan(int u,int pre){ st.push(u); instack[u] = true; vis[u] = true; dfn[u] = low[u] = ++dep; for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( !vis[v] ){ tarjan(v,u); low[u] = min( low[u], low[v] ); } else if( instack[v] ) low[u] = min( low[u], dfn[v] ); } if( dfn[u] == low[u] ) { int t; do{ id[ t=st.top() ] = conn; st.pop(); instack[t] = false; num[ conn ]++; // 该连通分量节点数量 }while( t != u ); conn++; } } int GetConnect(){ conn = 0; dep = 0; memset(vis,0,sizeof(vis)); memset(instack,0,sizeof(instack)); for(int i = 1; i <= n; i++) if( !vis[i] ) tarjan(i,0); /* for(int i = 1; i <= n; i++){ printf("dfn[%d] = %d, low[%d] = %d\n", i, dfn[i], i, low[i] ); printf("id[%d] = %d\n", i, id[i] ); }*/ return conn; } void solve() { GetConnect(); for(int u = 1; u <= n; u++) { for(int i = head[u]; ~i; i = edge[i].nxt ) { int v = edge[i].v; if( id[u] != id[v] ) D[ id[u] ]++; } } int ts = 0, rs = 0; for(int i = 0; i < conn; i++) { if( D[i] == 0 ) ts++, rs = i; } if( ts == 1 ) printf("%d\n", num[rs] ); else puts("0"); } int main() { //freopen("1.txt","r",stdin); while( scanf("%d%d", &n,&m) != EOF ) { input(); solve(); } return 0; }