桥与割点
前言:
就在前天,我学习了图的桥与割点。偷懒了两天后经过两天的理解,我,终于写写出了代码并写好了注释(^_^),今天就来写博客啦~
桥详细讲解:
首先,去找一张图。如下:
(这张图画得我好累)
那么,桥是什么呢?
在一个无向图G中,若去掉一条边e,使图G分裂为两个不相连的子图,则称边e为图G的桥或割边。
即在上图中,边5-7,边5-8为该图的桥。
让我们分别画出去掉这两条边的情况。
1.去边5-7:
图被分为不相连的两部分;
2.去边5-8:
图被分为不相连的两部分。
所以边5-7,边5-8为该图的桥。
肉眼看很容易,那么在计算机中,如何求解桥?
这里通过深度优先遍历(dfs)实现。
在此引入两个概念:
1.时间戳(dfn):dfn[ u ]表示节点u的深度优先遍历(dfs)序号,即节点u在深度优先遍历中的次序。(如,当第5次遍历时,dfn[ u ] = 5)
2.回溯点(low):low[ v ]表示节点v通过非父子边可回溯到的dfn最小值,即回到可回到的最早的节点。
举例如下图:
(不想画了,网上随便找的)
从节点1开始遍历,将节点1的dfn与low都赋值为1;
节点 | 1 |
dfn | 1 |
low | 1 |
接着按顺序走到节点2,同样将节点2的dfn与low赋值为2;
节点 | 1 | 2 |
dfn | 1 | 2 |
low | 1 | 2 |
以此类推到节点5,按顺序先遍历节点6,其他操作同上;
在遍历到节点4并赋值,表格如下:
节点 | 1 | 2 | 3 | 5 | 6 | 4 |
dfn | 1 | 2 | 3 | 4 | 5 | 6 |
low | 1 | 2 | 3 | 4 | 5 | 6 |
此时,判断节点4的儿子为节点1,已遍历,则将节点4的low值与节点1的dfn值比较,将节点4的low值赋值为较小的节点1的dfn值,然后返回,表格:
节点 | 1 | 2 | 3 | 5 | 6 | 4 |
dfn | 1 | 2 | 3 | 4 | 5 | 6 |
low | 1 | 2 | 3 | 4 | 5 | 1 |
节点6中,将节点6的low值与节点4的low值比较,将节点6的low值赋值为较小的节点4的low值,继续返回,节点5同理;
节点 | 1 | 2 | 3 | 5 | 6 | 4 |
dfn | 1 | 2 | 3 | 4 | 5 | 6 |
low | 1 | 2 | 3 | 41 | 1 | 1 |
此时,判断节点5还有一个未遍历的相连节点7,则遍历节点7;
节点7赋值后无相连节点,则low值最小为7,返回;
最后返回到节点1:
节点 | 1 | 2 | 3 | 5 | 6 | 4 | 7 |
dfn | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
low | 1 | 1 | 1 | 1 | 1 | 1 | 7 |
最终判断:通过表格可知,通过节点7只有一条路径可返回最早节点,所以只有一条路径可通向节点7,则这条路径为这张图的桥。
经过其实并不存在的n次的推理得出公式:当low[ v ]>dfn[ u ],即节点u的儿子节点v的low值大于节点u的dfn值时,边u-v为该图的桥。
上代码:
定义数组、变量啥的就不说了。啊对了,这里存储图用的是链式前向星,不会的可以看看我前几天的博客。
指路:链式前向星
1 int n,m;//图的顶点数,边数 2 int head[105],t;//头节点数组,数组e的计数变量 3 int low[105],dfn[105],num;//回溯数组,遍历数组,遍历次数 4 5 struct kk{//链式前向星 6 int to,next; 7 }e[250];
输入啥的我真不写了。
然后记得所有数组都要清零:
1 void init() 2 { 3 memset(head,0,sizeof(head));//头节点数组清零(因为要多次使用) 4 memset(low,0,sizeof(low));//回溯数组清零(因为要多次使用) 5 memset(dfn,0,sizeof(dfn));//遍历数组清零(因为要多次使用) 6 }
然后就是遍历啦:
1 //桥 2 for(int i=1; i<=n; i++)//从1开始,遍历每一个点 3 { 4 if(!dfn[i])//如果点i未遍历(即dfn[i]为零),遍历 5 { 6 qiao(i,0);//遍历点i,其父亲为零 7 } 8 }
函数:
1 void qiao(int u,int fa)//找桥(桥:即一条边,去掉这条边后可将这张图分为两部分) 2 { 3 dfn[u] = low[u] = num+1;//dfn[u]和low[u]赋值为遍历次数(即第i次遍历赋值为i),因为还未回溯,所以最开始两个数组数值相同 4 num++;//遍历次数加1 5 // cout << head[u] << endl; 6 for(int i=head[u]; i; i=e[i].next)//从头节点开始循环,循环到最后一个点u可直接到达的点 7 { 8 // cout << i << " " << e[i].to << " " << dfn[e[i].to] <<endl; 9 int v=e[i].to;//定义一个变量为e[i].to,便于比较 10 if(v==fa)//如果e[i].to等于点u的父亲,则跳过这次循环,去遍历下一个点 11 { 12 // cout << "fa: " << fa << endl; 13 continue;//跳过本次循环 14 } 15 if(!dfn[v])//如果点v未遍历,即dfn[v]为零,则遍历点v,其父亲为u 16 {// 17 // cout << i << endl; 18 // cout << u << " " << v << endl; 19 qiao(v,u);//遍历 20 // cout << low[u] << " " << low[v] << endl; 21 low[u] = min(low[u],low[v]);//点u可通过非父子边回到的最小值为low[u]和low[v]中的最小数 22 if(low[v]>dfn[u])//判定定理:如果low[v]>dfn[u],则点u为桥 23 { 24 cout << u << "--" << v << endl;//输出 25 } 26 } 27 else//若遍历过,则找点u可通过非父子边回到的最小值(若v遍历过,则dfn[v]一定不比low[u]大,否则点v为遍历) 28 { 29 low[u] = min(low[u],dfn[v]); 30 } 31 } 32 }
就结束啦~
然后是割点:
割点详解:
没错还是这张图。
割点就是将这个点从图中去掉后会将图分为至少两个互不相连的子图。
其实除了判断方法,这俩其他一模一样,就不讲了哈。
看看判断:
1.若节点u为根节点,low[ v ]>=dfn[ u ],u为割点;
2.若节点u不为根节点,low[ v ]>=dfn[ u ],且节点u有至少两个子节点,u为割点。
因为桥和割点长得真的一模一样,所以不多讲,上代码:
1 for(int i=1; i<=n; i++)//从1开始,遍历每一个点 2 { 3 if(!dfn[i])//如果点i未遍历(即dfn[i]为零),遍历 4 { 5 root = i;//因为要找割点,所以先将根赋值为i 6 gd(i,0);//遍历点i,其父亲为零 7 } 8 }
函数:
1 void gd(int u,int fa)//找割点(割点:即某个顶点,去掉该顶点后可将一个图分为大于等于2个图) 2 { 3 dfn[u] = low[u] = num+1;//dfn[u]和low[u]赋值为遍历次数(即第i次遍历赋值为i),因为还未回溯,所以最开始两个数组数值相同 4 num++;//遍历次数加1 5 int cnt=0;//记录本次遍历中low[v]>=dfn[u]的次数(即点u能直接通向的点/儿子数) 6 // cout << head[u] << endl; 7 for(int i=head[u]; i; i=e[i].next)//从头节点开始循环,循环到最后一个点u可直接到达的点 8 { 9 // cout << i << " " << e[i].to << " " << dfn[e[i].to] <<endl; 10 int v=e[i].to;//定义一个变量为e[i].to,便于比较 11 if(v==fa)//如果e[i].to等于点u的父亲,则跳过这次循环,去遍历下一个点 12 { 13 // cout << "fa: " << fa << endl; 14 continue;//跳过本次循环 15 } 16 if(!dfn[v])//如果点v未遍历,即dfn[v]为零,则遍历点v,其父亲为u 17 { 18 // cout << i << endl; 19 // cout << u << " " << v << endl; 20 gd(v,u);//遍历 21 // cout << low[u] << " " << low[v] << endl; 22 low[u] = min(low[u],low[v]);//点u可通过非父子边回到的最小值为low[u]和low[v]中的最小数 23 if(low[v]>=dfn[u])//判定定理:如果low[v]>=dfn[u],则点u可能为割点 24 { 25 cnt++;//儿子数加1 26 if(u!=root || cnt>1)//如果点u不为根,或点u为根且有不小于2个儿子,当low[v]>=dfn[u]时,点u为割点 27 { 28 cout << u << endl;//输出 29 } 30 } 31 } 32 else//若遍历过,则找点u可通过非父子边回到的最小值(若v遍历过,则dfn[v]一定不比low[u]大,否则点v为遍历) 33 { 34 low[u] = min(low[u],dfn[v]); 35 }
OK~
完整代码:
1 #include<iostream> 2 #include<string> 3 #include<cstring> 4 #include<queue> 5 #include<algorithm> 6 7 using namespace std; 8 9 int n,m;//图的顶点数,边数 10 int head[105],t,root;//头节点数组,数组e的计数变量,记录根节点 11 int low[105],dfn[105],num;//回溯数组,遍历数组,遍历次数 12 13 struct kk{//链式前向星 14 int to,next; 15 }e[250]; 16 17 void init() 18 { 19 memset(head,0,sizeof(head));//头节点数组清零(因为要多次使用) 20 memset(low,0,sizeof(low));//回溯数组清零(因为要多次使用) 21 memset(dfn,0,sizeof(dfn));//遍历数组清零(因为要多次使用) 22 } 23 24 void qiao(int u,int fa)//找桥(桥:即一条边,去掉这条边后可将这张图分为两部分) 25 { 26 dfn[u] = low[u] = num+1;//dfn[u]和low[u]赋值为遍历次数(即第i次遍历赋值为i),因为还未回溯,所以最开始两个数组数值相同 27 num++;//遍历次数加1 28 // cout << head[u] << endl; 29 for(int i=head[u]; i; i=e[i].next)//从头节点开始循环,循环到最后一个点u可直接到达的点 30 { 31 // cout << i << " " << e[i].to << " " << dfn[e[i].to] <<endl; 32 int v=e[i].to;//定义一个变量为e[i].to,便于比较 33 if(v==fa)//如果e[i].to等于点u的父亲,则跳过这次循环,去遍历下一个点 34 { 35 // cout << "fa: " << fa << endl; 36 continue;//跳过本次循环 37 } 38 if(!dfn[v])//如果点v未遍历,即dfn[v]为零,则遍历点v,其父亲为u 39 {// 40 // cout << i << endl; 41 // cout << u << " " << v << endl; 42 qiao(v,u);//遍历 43 // cout << low[u] << " " << low[v] << endl; 44 low[u] = min(low[u],low[v]);//点u可通过非父子边回到的最小值为low[u]和low[v]中的最小数 45 if(low[v]>dfn[u])//判定定理:如果low[v]>dfn[u],则点u为桥 46 { 47 cout << u << "--" << v << endl;//输出 48 } 49 } 50 else//若遍历过,则找点u可通过非父子边回到的最小值(若v遍历过,则dfn[v]一定不比low[u]大,否则点v为遍历) 51 { 52 low[u] = min(low[u],dfn[v]); 53 } 54 } 55 } 56 57 void gd(int u,int fa)//找割点(割点:即某个顶点,去掉该顶点后可将一个图分为大于等于2个图) 58 { 59 dfn[u] = low[u] = num+1;//dfn[u]和low[u]赋值为遍历次数(即第i次遍历赋值为i),因为还未回溯,所以最开始两个数组数值相同 60 num++;//遍历次数加1 61 int cnt=0;//记录本次遍历中low[v]>=dfn[u]的次数(即点u能直接通向的点/儿子数) 62 // cout << head[u] << endl; 63 for(int i=head[u]; i; i=e[i].next)//从头节点开始循环,循环到最后一个点u可直接到达的点 64 { 65 // cout << i << " " << e[i].to << " " << dfn[e[i].to] <<endl; 66 int v=e[i].to;//定义一个变量为e[i].to,便于比较 67 if(v==fa)//如果e[i].to等于点u的父亲,则跳过这次循环,去遍历下一个点 68 { 69 // cout << "fa: " << fa << endl; 70 continue;//跳过本次循环 71 } 72 if(!dfn[v])//如果点v未遍历,即dfn[v]为零,则遍历点v,其父亲为u 73 { 74 // cout << i << endl; 75 // cout << u << " " << v << endl; 76 gd(v,u);//遍历 77 // cout << low[u] << " " << low[v] << endl; 78 low[u] = min(low[u],low[v]);//点u可通过非父子边回到的最小值为low[u]和low[v]中的最小数 79 if(low[v]>=dfn[u])//判定定理:如果low[v]>=dfn[u],则点u可能为割点 80 { 81 cnt++;//儿子数加1 82 if(u!=root || cnt>1)//如果点u不为根,或点u为根且有不小于2个儿子,当low[v]>=dfn[u]时,点u为割点 83 { 84 cout << u << endl;//输出 85 } 86 } 87 } 88 else//若遍历过,则找点u可通过非父子边回到的最小值(若v遍历过,则dfn[v]一定不比low[u]大,否则点v为遍历) 89 { 90 low[u] = min(low[u],dfn[v]); 91 } 92 } 93 } 94 95 int main() 96 { 97 while(cin >> n >> m && n && m)//当输入n,m且n,m都不为零时开始循环 98 { 99 t = num = 0;//最开始先让数组e的计数和遍历次数为零 100 init();//各种数组清零 101 for(int i=1; i<=m; i++)//输入边 102 { 103 int u,v;//临时变量 104 cin >> u >> v;//输入,表示点u与点v之间有边 105 e[t+1].to = v;//数组e从1开始计数 106 e[t+1].next = head[u];//链式前向星相关 107 head[u] = t+1;//链式前向星相关 108 t++;//数组e的计数加1,开始下一轮赋值 109 e[t+1].to = u;//因为是无向图,所以两个方向(u-v,v-u)都赋值一遍 110 e[t+1].next = head[v];//链式前向星相关 111 head[v] = t+1;//链式前向星相关 112 t++;//数组e的计数加1,便于下一轮赋值(我真是太会水字数啦哈哈哈~) 113 // cout << head[u] << " " << head[v] << endl; 114 } 115 116 //桥 117 for(int i=1; i<=n; i++)//从1开始,遍历每一个点 118 { 119 if(!dfn[i])//如果点i未遍历(即dfn[i]为零),遍历 120 { 121 // root = i; 122 qiao(i,0);//遍历点i,其父亲为零 123 } 124 } 125 // cout << "dsfsd"; 126 127 //割点 128 //我学的时候是割点和桥分开写的,但此处为了方便将两个代码合并了,会有很多重复(因为代码差不多),完全可以写在一个函数里。为了便于理解分开写了 129 memset(low,0,sizeof(low));//因为找割点又要遍历一次,所以回溯数组清零 130 memset(dfn,0,sizeof(dfn));//遍历数组清零 131 num = 0;//遍历次数清零 132 for(int i=1; i<=n; i++)//从1开始,遍历每一个点 133 { 134 if(!dfn[i])//如果点i未遍历(即dfn[i]为零),遍历 135 { 136 root = i;//因为要找割点,所以先将根赋值为i 137 gd(i,0);//遍历点i,其父亲为零 138 } 139 } 140 // for(int i=1; i<=n; i++) 141 // { 142 // cout << dfn[i] << " " << low[i] << endl; 143 // } 144 cout << "gfdg";//随便打的,表示本次循环结束,可以再次输入了 145 } 146 return 0; 147 }
嘿嘿~
练习:P3388
代码:
1 #include<iostream> 2 #include<string> 3 #include<cstring> 4 #include<queue> 5 #include<algorithm> 6 7 using namespace std; 8 9 int n,m; 10 int dfn[100005],low[100005],num,root; 11 int data[100005],tt; 12 13 int head[100005]; 14 struct kk{ 15 int to,next; 16 }e[200005]; 17 int t; 18 19 void gd(int u,int fa) 20 { 21 int cnt=0; 22 num++; 23 dfn[u] = low[u] = num; 24 for(int i=head[u]; i; i=e[i].next) 25 { 26 int v=e[i].to; 27 if(v==fa) 28 { 29 continue; 30 } 31 if(!dfn[v]) 32 { 33 34 gd(v,u); 35 low[u] = min(low[u],low[v]); 36 if(low[v]>=dfn[u] && u!=root) 37 { 38 if(!data[u]) tt++; 39 data[u] = 1; 40 } 41 if(u==root) 42 { 43 cnt++; 44 } 45 } 46 low[u] = min(low[u],dfn[v]); 47 } 48 if (cnt>=2 && u==root) 49 { 50 if(!data[u]) tt++; 51 data[u]=1; 52 } 53 } 54 55 int main() 56 { 57 cin >> n >> m; 58 for(int i=1; i<=m; i++) 59 { 60 int x,y; 61 cin >> x >> y; 62 t++; 63 e[t].to = y; 64 e[t].next = head[x]; 65 head[x] = t; 66 t++; 67 e[t].to = x; 68 e[t].next = head[y]; 69 head[y] = t; 70 } 71 for(int i=1; i<=n; i++) 72 { 73 if(!dfn[i]) 74 { 75 root = i; 76 gd(i,0); 77 } 78 } 79 cout << tt << endl; 80 for(int i=0; i<=n; i++) 81 { 82 if(data[i]) cout << i << " "; 83 } 84 return 0; 85 }
完成!
谢谢观看(^_^)