在学习了刘汝佳大大的蓝书后有点体会,特来小结一下.
割点:对于无向图G,如果删除某个点u后,连通分量数目增加,称u为图的割点.
桥:对于无向图G,如果删除某条边(u,v)后,连通分量数目增加,称(u,v)为图的桥.
先说说怎么求割点吧,我们可以先想怎么暴力求解:把每一个点删除一次,然后dfs看连通分量的数目的变化,不过这样复杂度O(n(n + m)),能不能优化呢?先想象一下割点是一个什么样的点:
其中A很显然是割点,D不是割点,这两个点很有特点,所以我们拿它们来分析,这个图很像一棵树,其中A是根节点,我们可以猜想一下:如果一棵树的根节点有两个或两个以上个孩子,那么根节点就是割点,这个是显然的.那么D为什么不是割点呢?C是D的祖先,F是D的儿子,儿子能连一条边到祖先,所以D就不是割点。这些分析都建立在图是一棵树的基础上,可是这根本不是树啊?我们可以考虑dfs树:
以dfs建立的一棵树(虽然并不是树)就能满足我们的要求,由此可以得到一条定理:在无向连通图G的DFS树中,非根节点u是G的割点当且仅当u存在一个子节点v,使得v及其后代都没有反向边连回u的祖先(连回u不算).
证明很简单,如果有边连回祖先,那么删除这个点后,剩下的点都能通过这条边组成一个连通分量,数目不变.
现在考虑怎么实现这个算法,设low(u)为u及其后代能连回的最早的祖先的pre值,pre值是我们认为规定的一个值,定义是dfs到节点u时,pre[u] = ++dfs_clock, dfs_clock是一个累加器,目的是为了记录到每个节点的时间.那么u是割点当且仅当low(v) >= pre(u),v是u的子节点.这时有一个特殊情况:v的后代只能连回v自己,即low(v) > pre(u),这个时候边(u,v)和(v,u)就是桥,具体的证明可以参考上面割点的证明.
算法的具体实现:
int dfs(int u, int fa) { int lowu = pre[u] = ++dfs_clock; int child = 0; for (int i = head[u]; i + 1; i = nextt[i]) { int v = to[i]; if (!pre[v]) { child++; int lowv = dfs(v, u); lowu = min(lowv, lowu); if (lowv >= pre[u]) //记录割顶 flag[u] = true; if (lowv > pre[u]) //记录桥 flag2[u][v] = flag2[v][u] = 1; } else if (pre[v] < pre[u] && v != fa) //千万不要漏掉第二个条件,这是(fa,u)的第二次访问,不是指向祖先的边! lowu = min(lowu, pre[v]); } if (fa < 0 && child == 1) //特判根节点 flag[u] = 0;return lowu; }
初始化的时候pre全部为0,开始dfs的时候fa = -1,基本上注意点都在代码里了,至于为什么我们遇到割点不直接输出是因为一个割点可能有多个子节点满足条件,防止重复输出.