在一个无向图中,若删除某一个节点,使得图分成若干个不相联通的子图,那么,这个节点就是图的割点。就像下图所示:
节点1、节点3和节点4,都是这个图的割点。为什么呢?你想啊,把节点1、节点3和节点4遮住,会怎么样?节点2永远也没法去到6 5 8 7 那些,同理,遮住节点4后,7,8也没法去到6 5 了。这样,就形成了一个个独立子图了。
那么,我们怎么知道这个点,就是割点呢?
①、用两个DFS,删除某个节点后,对其后面的节点进行DFS,如果在【不同过该节点】的情况下,不能回到以前的节点,那么,被删除的节点就是割点啦。删除?怎么删除,其实只需一个book数组,book[i]=1;DFS后,book[i]=0;就OK 啦。。起到一个临时标记,也就起到了"删除"的效果了。最后,book[i]=0;是要把删除的节点补回来。我们没理由弄破一个图吧?  ̄□ ̄|| 最后,该算法的复杂度是O(N(N+M)); 怎样?很复杂吧?有没有其他优秀的算法呢?
②、只需一个DFS,不过要增加两个数组:
一个是num[];//记录的是这个顶点在遍历时,是第几个被访问到的
一个是low[];//表示一个节点,在不经过其父亲时,能回到最早的祖先是谁
::>_<:: 。。什么是父亲?祖先又是谁?这里我们把问题抽象化一下,在上图中,假设我们从1号顶点开始DFS,那么,1顶点能直接去到的顶点,我们称其为1号顶点的儿子,那么,一号顶点就是其父亲。这里需要记住一点:【父亲的num[i]】总是比儿子的少,因为辈分越大,数值越小,我们可以理解为,我爷爷是第一代传人,爸爸是第二代,我是第三代。记住这点很重要哦⊙▂⊙。
那么,假如一个顶点cur,在不经过其父亲顶点father的时候,总能回到更早的顶点(father的祖先),那么,这个father就不是割点。就比如图中的5号顶点,(根据上面,此时的father=5,cur=4),如果删除了5号顶点,cur还是能够通过6号顶点回到3顶点,甚至能回到1号顶点,此时,cur就能回到father的祖先了,此时,father=5并不是图的割点。
相反,如果一个顶点cur,在不经过其父亲顶点father的时候,总不能回到更早的顶点(father的祖先),那么,这个father就是割点啦!比如图中的4号顶点。
那么,我们怎么判断它不能回到自己【父亲的祖先呢】?其实只需要low[cur]>=num[father];就是,儿子能回到最早的祖先,最多最多,也只是它的爸爸。也就是,我不问过我爸爸,我连我爷爷是谁都不知道,那么,爸爸起到连通我和我爷爷的作用,所以,我爸爸就是割点了。 ̄□ ̄||。 我们再来形象理解下这句代码low[cur]>=num[father];我们用辈分来论事,记得我上面说过一句话很重要的吗?拉上去看看吧。这里,我先说个小故事。
话说,在古代,风家人才辈出,能才能武,祖祖祖祖祖祖祖祖爷也还在世上。 ̄□ ̄||。要知道,在这些高人辈出的家族里,有些长辈不是你想见就能见的,有些族人一生在世,没见过爷爷一面也有可能,因为高手一般都是闭关的。。但是如果经过父亲的推荐,那就不同了。一般儿子是绝对知道自己的父亲的,这点不用我说了吧。 ̄□ ̄||。所以cur的father就向他的父亲(就是cur的爷爷)说:我儿子cur天生神力,资质颇高,父亲你就点拔他一些武功吧,爷爷说,"哦,好好好,报名费5000大洋!"这样,cur就能知道他爷爷是谁了。但是,如果cur的父亲father不是伯乐,不知道cur是千里马,那就麻烦了,cur会被埋没的。就这样,cur一辈子平平淡淡,真是伤仲永哇,cur的父亲father,成了cur的"割点"了,就是,cur在不经过他爸爸father的推荐下,不能知道他的爷爷,爷爷爷,爷爷爷爷是谁,那么此时,cur只认识他的爸爸。说来说去,你还没告诉我low[cur]>=numer[father]是什么嘛!我该怎么判断cur是否只认识他爸爸呢?(请把这两句话看3遍)。没错,你想到了,low[cur]>=number[father];就是表明cur最多只认识他的爸爸。low[cur]==number[father]的时候,就表明cur知道自己的爸爸。为什么是?>=而不是<=呢?我们看看辈分?爷爷是第一辈,爸爸是第二辈,我是第三辈。如果我知道我爷爷是谁,low[cur]=1了,而number[father]=2;此时,low[cur]<number[father]。也就说明我能去到爷爷,father不是割点。
number[]//其实number代表的是自己的辈分,
low[];//代表我能认识到最老的长辈。注意,越老,值越小。因为辈分是1
说了那么多,我们来看看完整的代码?
#include <stdio.h> #include <stdlib.h> int n,m;//n代表有多少个节点,m代表有多少条边 //要注意m>=n-1最小也要有n-1条边才能把图接上 int e[51][51];//用邻接表来存一个图 int num[51],low[51]; int flag[51];//用来表示哪一个是割点 int root;//根节点。 int first;//表示一开始num和low的值 int min (int x,int y) { return x>y?y:x; } void dfs (int cur,int father) { int child=0;//代表cur有多少个儿子 int i;//用来循环的而已 first++;//这个我们称为时间戳 num[cur] = first;//刚开始的辈分 low[cur] = first;//刚开始能访问到的,只能是自己 for (i=1;i<=n ;i++ )//循环一个图 { if (e[cur][i]==1)//如果cur去i是有路的 { if (num[i]==0)//就是i还没被访问过。先跳去看else { child++;//儿子数+1 dfs (i,cur);//分清楚谁是爸爸,谁是儿子,此时很 //明显cur是i的爸爸 //结束dfs后 low [cur] = min(low[cur],low[i]);//更新我和儿子能访问到的人 //如果我儿子能访问到我太爷,但是我爸爸又不推荐我去见我太爷 //那么,我可以通过儿子去见啊,父之命,能不从? if (cur != root && low[i]>=num[cur]) //此时儿子是i 父亲是cur //如果我不是第一辈的人,并且,我的儿子是渣渣,失去我的推荐后 //自己不能去到我的爸爸或者更早的人 //那么,我成了我儿子的割点了 //其实究竟是我伤仲永,还是我儿子本来就不是千里马呢?哈哈 //有些东西,一个巴掌拍不响 { flag[cur] = 1;//我是割点 } if (cur == root &&child==2) //如果我是第一辈,并且我有两个儿子 { flag[cur]=1; } } else if (i!=father)//如果节点i被访问过 //但是,这个i不是cur的父亲father。我们动起笔,模拟一下这个过程 //首先(先不看1-->2)1-->3,到3的dfs,for循环中,第一个值i=1,就是 //3访问他父亲了,这是没意思的,但是我们又不能book标记它,这样会造成很多 //节点访问不到的,所以,如果访问到的节点是父亲,我们不做任何事情。 //就是上面else if不成立,但是,如果不是呢?就是6能去到3了,而6号的父亲是 //4啊。。那么,恭喜你,你拜见了你的爷爷爷 爷了,快去拜师吧 { low[cur] = min(low[cur],num[i]);//拜师中 //注意此时的cur是6,i是3 //就是,cur去到的辈分的i的辈分 //也就是num[i]。当然low[cur]比num[i]更小就另当别论了 //也就是cur见到了太太太爷。。。哇靠!神人啊。 //太爷出来见他 } } } } void work() { scanf ("%d%d",&n,&m); int i,j; for (i=1;i<=n ; i++) { for (j=1;j<=n ;j++ ) { e[i][j] = 0;//初始化一个图 } } for (i=1;i<=m ;i++ ) { int u,v; scanf ("%d%d",&u,&v); e[u][v] = 1;//表示有路可去 e[v][u] = 1;//无向图嘛 } root=1;//先把root设置成1 dfs (1,root);//从节点1开始,把节点1当成是根节点 for (i=1;i<=n ;i++ ) { if (flag[i]) { printf ("%d ",i);//输出割点 } } return ; } int main() { work();//调用一个函数来分担主函数的代码 return 0; }
对了,我还没说过child和root的事情,root代表的是祖爷(第一代传人,就是家主)。因为家主没可能成为其儿子的割点啊,家主都没父亲(他从石头爆出来的),那么1为什么还是割点啊,这里他是妨碍了自己的儿子见面,他的儿子也是的,师兄都不见面,各立一派吗?哦,对哦,家主之位,岂能乱传?所以,一般古代那些皇帝的儿子,都是闹不和的,为什么?大家都想自己的爸爸给皇帝位给我啊。!!!
child代表的是儿子,要注意,这里的child==2不代表家主只有一个儿子,代表的是,家主闹不和的儿子达到了两个,家主就是割点了。。最后,希望我们能动笔把每次dfs都写出来,这样,理解起来,更加的好、、
本人初出茅庐,有什么不正确的地方,希望朋友们能指教下,我感激不尽。。共同努力