双连通图:在无向图连通图中,如果删除该图中的任意一点和依附它的边,不改变图的连通性,则称该图为双连通的无向图。
由上述定义可知,双连通分量中,每两个结点之间至少有两条不同的路径可以相互到达。
割点:在无向连通图中删去某个点a和依附a的边,图变为不连通,则该点称为割点,也叫关节点。
割边:在无向连通图中删去某条边,图变为不连通,则该边称为割边,也叫桥。
点双连通分支(块)与边双连通分支:
点双连通分支与边双连通分支是两个完全不同的概念。割点可以存在多个点连通分支中(相反,桥就不一样)。一个图可以有割点而没有割边,也可以有割边而没有割点。
点双连通分支的求法和边双连通分支的求法类似,不过在出栈的地方有些不同。下面介绍几个例子。
POJ3177(3352)
题目大意:最少需要加多少条边使得原图变为双连通图(原图连通)。
解:求桥(注意平行边),在求桥的过程中缩点,利用并查集,将每个连通分量用一个点代表。将这些代表用桥连接起来,就构成了一颗树。统计树中度数为1的点(即叶子结点)的个数count,将叶子结点两两相连,则添加边的数量为(count+1)/2。
#include <iostream> const int MAX = 5002; int p[MAX]; struct Graph { int to; int next; }e[MAX*4]; int index[MAX]; int edgeNum; int seq; int low[MAX]; //low[u]表示在树中从u点出发,经过一条其后代组成的路径和回退边,所能到达的最小深度的顶点标号 int dfn[MAX]; //dfn[u]表示结点u在树中的编号 int bridge[MAX][2],bridge_n; int degree[MAX]; int n,m; int min(int x, int y) { return x < y ? x : y; } void makeSet() { for(int i = 1; i <= n; i++) p[i] = i; } int findSet(int x) { if(x != p[x]) p[x] = findSet(p[x]); return p[x]; } void Union(int x, int y) { x = findSet(x); y = findSet(y); if(x == y) return; p[y] = x; } void addEdge(int from, int to) { e[edgeNum].to = to; e[edgeNum].next = index[from]; index[from] = edgeNum++; e[edgeNum].to = from; e[edgeNum].next = index[to]; index[to] = edgeNum++; } //边连通分量,求桥 void bridge_dfs(int u, int v) { int repeat = 0; //有平行边 low[u] = dfn[u] = seq++; for(int i = index[u]; i != -1; i = e[i].next) { int w = e[i].to; if(w == v) repeat++; if(dfn[w] < 0) { bridge_dfs(w,u); low[u] = min(low[u],low[w]); if(!(low[w] > dfn[u])) //不是桥,缩点 { Union(w,u); } else { bridge[++bridge_n][0] = u; bridge[bridge_n][1] = w; } } else if(v != w || repeat != 1) //重要 low[u] = min(low[u],dfn[w]); } } int solve() { int i,j; int a,b; int count = 0; memset(degree,0,sizeof(degree)); for(i = 1; i <= bridge_n; i++) { a = findSet(bridge[i][0]); b = findSet(bridge[i][1]); degree[a]++; degree[b]++; } for(i = 1; i <= n; i++) if(degree[i] == 1) count++; return (count+1)/2; } int main() { int i,j; int a,b; edgeNum = 0; seq = 0; bridge_n = 0; memset(index,-1,sizeof(index)); memset(dfn,-1,sizeof(dfn)); scanf("%d %d",&n,&m); for(i = 0; i < m; i++) { scanf("%d %d",&a,&b); addEdge(a,b); } makeSet(); bridge_dfs(1,-1); printf("%d\n",solve()); return 0; }
POJ2942
题目大意:有n个骑士,骑士一段时间要坐在圆桌上举行高级会议,但要满足条件:互相憎恨的骑士不能相邻,圆桌上的人数必须是大于1的奇数。现在给出骑士之间的憎恨关系,问至少有多少个骑士要被排除在外。
解:首先建补图,这样骑士a和骑士b之间有连线,说明a和b可以相邻。还可以想到奇环,不在任何奇环的骑士将被排除。问题是如何求图中的奇环?这里有一个定理:双连通分量中如果存在奇环,那么整个分量的点全部包含在奇环中(自己体会)。这样,可以先求点的双连通分量,判断每个双连通分量是否包含奇环(用染色,若相邻点颜色相同,存在奇环),若存在奇环,则该分量包含的骑士都不会被排除。
#include <iostream> const int MAX = 1001; int n,m; //若某块(双连通分量)不可染色为二分图,则该块存在奇圈;若某块存在奇圈,那么该块中的所有点都存在与奇圈中; //那么答案就是所有不在任何奇圈中的骑士的个数。 bool map[MAX][MAX]; int dfn[MAX],low[MAX],stack[MAX]; int top,seq,result; bool b[MAX],used[MAX]; int color[MAX]; int min(int x, int y) { return x < y ? x : y; } bool isOk(int v, int col) { color[v] = col; for(int w = 1; w <= n; w++) { if(map[v][w]) { if(b[w]) { if(color[v] == color[w]) //相邻两点颜色相同,构不成二分图,含奇圈 return true; if(color[w] == -1) isOk(w, col^1); } } } return false; } void dummy(int t, int *a) { int i,j; memset(b,0,sizeof(b)); //b[i]=1表示结点i属于当前的双连通分量中 for(i = 0; i < t; i++) b[a[i]] = true; for(i = 0; i < t; i++) { memset(color,-1,sizeof(color)); if(isOk(a[i],1)) break; } if(i < t) //含奇圈 { for(j = 0; j < t; j++) { if(!used[a[j]]) { result++; used[a[j]] = true; } } } } void bicon(int u) { int a[MAX]; low[u] = dfn[u] = seq++; stack[top] = u; top++; for(int w = 1; w <= n; w++) { if(map[u][w]) { if(dfn[w] < 0) //第一种情况,w是新点 { bicon(w); low[u] = min(low[u],low[w]); if(low[w] >= dfn[u]) //u割点(把割点留在栈中) { int k = 1; a[0] = u; do { --top; a[k++] = stack[top]; }while(stack[top] != w); dummy(k,a); } } else //u,w是回边(w是u的祖先) low[u] = min(low[u],dfn[w]); } } } void block() { for(int i = 1; i <= n; i++) if(dfn[i] < 0) bicon(i); } int main() { int i,j; int a,b; while(true) { scanf("%d %d",&n,&m); if(n == 0 && m == 0) break; memset(map,1,sizeof(map)); memset(dfn,-1,sizeof(dfn)); memset(used,0,sizeof(used)); seq = 0; top = 0; result = 0; for(i = 0; i < m; i++) { scanf("%d %d",&a,&b); map[a][b] = false; map[b][a] = false; } for(i = 1; i <= n; i++) map[i][i] = false; block(); printf("%d\n",n - result); } return 0; }
POJ3694
题目大意:给定一个初始的网络,每次(1000次)向网络里加一条边,问网络中桥的数量。(网络是动态的)
解:这题难就难在网络是动态的,如果是静态,可以用边的双连通分量来直接求解。简单的想法是每修改一次就重新计算一次,但是这样超时。联想:通过缩点,缩点之间用桥连接,形成一颗树,树边就是桥,桥的总数为sum。每次向网络里加一条边a,b,先用并查集找出a和b所属的树的结点,显然,a和b到ab的最近公共祖先这条路径上的桥全部无效。这样,每次只需在树上操作sum--。这样复杂度就降下来了。
#include <iostream> const int MAX = 100002; int n,m; int result; struct Edge { int to; int next; }e[MAX*10],tree[MAX*10]; int index[MAX],index2[MAX],edgeNum,edgeT; int seq; int low[MAX],dfn[MAX]; int p[MAX],res[MAX]; int level[MAX],pre[MAX]; bool vis[MAX],bridge[MAX]; int min(int x, int y) { return x < y ? x : y; } void addEdge(int from, int to) { e[edgeNum].to = to; e[edgeNum].next = index[from]; index[from] = edgeNum++; } void addTree(int from, int to) { tree[edgeT].to = to; tree[edgeT].next = index2[from]; index2[from] = edgeT++; } void makeSet() { for(int i = 1; i <= n; i++) p[i] = i; } int findSet(int x) { if(x != p[x]) p[x] = findSet(p[x]); return p[x]; } void Union(int x, int y) { x = findSet(x); y = findSet(y); if(x == y) return; p[x] = y; } void bridge_dfs(int u, int v) { int repeat = 0; low[u] = dfn[u] = seq++; for(int i = index[u]; i != -1; i = e[i].next) { int w = e[i].to; if(v == w) repeat++; if(dfn[w] < 0) { bridge_dfs(w,u); low[u] = min(low[u],low[w]); if(low[w] > dfn[u]) { result++; res[result] = i; //bridge[w] = 1; } else Union(w,u); } else if(v != w || repeat != 1) low[u] = min(low[u],dfn[w]); } } void lca_dfs(int u, int deep) { for(int i = index2[u]; i != -1; i = tree[i].next) { int v = tree[i].to; if(!vis[v]) { vis[v] = true; pre[v] = u; level[v] = deep+1; lca_dfs(v,deep+1); } } } void lca(int u, int v) { while(level[u] > level[v]) { if(bridge[u]) { result--; bridge[u] = 0; } u = pre[u]; } while(level[v] > level[u]) { if(bridge[v]) { result--; bridge[v] = 0; } v = pre[v]; } while(u != v) { if(bridge[u]) { bridge[u] = 0; result--; } if(bridge[v]) { bridge[v] = 0; result--; } u = pre[u]; v = pre[v]; } } int main() { int i,j; int a,b; int q; int cases = 1; while(true) { scanf("%d %d",&n,&m); if(n == 0 && m == 0) break; edgeNum = 0; edgeT = 0; result = 0; seq = 0; memset(index,-1,sizeof(index)); memset(index2,-1,sizeof(index2)); memset(dfn,-1,sizeof(dfn)); memset(vis,0,sizeof(vis)); memset(level,0,sizeof(level)); memset(bridge,0,sizeof(bridge)); makeSet(); for(i = 0; i < m; i++) { scanf("%d %d",&a,&b); addEdge(a,b); addEdge(b,a); } scanf("%d",&q); printf("Case %d:\n",cases++); bridge_dfs(1,-1); //找到边的双连通 缩点 int x,y; for(i = 1; i <= n; i++) //将缩点集合转化为一颗树 { for(j = index[i]; j != -1; j = e[j].next) { x = findSet(i); y = findSet(e[j].to); if(x != y) addTree(x,y); } } // memset(vis,0,sizeof(vis)); vis[p[1]] = true; level[p[1]] = 1; lca_dfs(p[1],1); for(i = 1; i <= result; i++) bridge[findSet(e[res[i]].to)] = 1; while(q--) { scanf("%d %d",&a,&b); a = findSet(a); b = findSet(b); if(a != b) lca(a,b); printf("%d\n",result); } printf("\n"); } return 0; }