20251227 - 点双 割点 割边 总结
比赛链接:https://vjudge.net/contest/777735#problem/A。
割点
介绍
如果从无向图中删去一个点之后,图里的连通分量的数量增加了,那么删去的这个点就被称之为割点。
暴力做法,考虑依次删除每个点,再去数连通分量的个数,判断是否为割点,那么时间复杂度就是 \(O(nm)\) 的,太慢了。一般都是用 Tarjan 做到 \(O(n+m)\) 找割点的。
做法
Tarjan 时,会在 DFS 的过程中维护 \(dfn\) 和 \(low\) 两个信息,其中 \(low_u\) 是指从 \(u\) 出发且最多用反祖边向上走一步的情况下能到达的最小的时间戳。
如果一个点 \(u\),有子节点 \(v\) 不能回到比 \(u\) 更高的节点,那么 \(u\) 就是割点。因为删掉 \(u\) 之后,\(v\) 所在的子树没有反祖边可以连回来,那么图的连通分量数量就会增多,那么 \(u\) 就是割点咯。
我们可以用 \(low\) 和 \(dfn\) 两个数组快速判断是否能连回来,如果 \(low_v \ge dfn_u\) 就代表 \(v\) 这个子节点连不上去了。
这个判断法则只在非根节点时成立。
根节点需要特殊处理。如果根节点在 DFS 树上有两个或更多子节点,根节点是割点,否则如果只有一个子节点就不是割点。证明过于显然所以不写了。
实现
我在代码中用 \(id\) 表示上文中提到的 \(dfn\)。
void DFS(int u,int fa){
id[u]=(++tot),low[u]=id[u];int cnt=0;
for(int v:g[u]){
if(v==fa)continue;
if(id[v])low[u]=min(low[u],id[v]);
else{
DFS(v,u),cnt++;
low[u]=min(low[u],low[v]);
if(fa&&low[v]>=id[u])cut[u]=1;
}
}if(!fa&&cnt>1)cut[u]=1;return;
}
做法 2
请先把下面的【点双连通分量】部分看完喵。
我们根据割点和点双连通分量的定义,一点也不敏锐地发现一个规律,如果某个点是割点,那么它一定出现在多个点双连通分量中。
那么我们就可以根据这个性质来找割点了。
代码就不放了。其实是因为没写过喵 TAT
割边
介绍
割边也叫桥。
它和割点的定义非常类似:如果从无向图中删去一条边之后,图里的连通分量的数量增加了,那么删去的这个边就被称之为割边。
做法
如果图中没有重边,那么我们只需要把求割点的过程中的 \(low_v \ge dfn_u\) 中的 \(\ge\) 改为 \(>\) 即可,因为割点是以点为单位,割边是以边为单位,但 \(low\) 和 \(dfn\) 记录的都是点的时间戳。
但是,如果图中有重边,那么重边们一定不是割边——显然,你删掉这条后还有另外一条一模一样的,那怎么也不会让图里的连通分量增多。
怎么处理呢?有一种方法,那就是在遍历到从子节点走向父节点的边时,把第一条这样的边忽略,出现第二条时才按照正常的反祖边处理,这样就能在不动原先逻辑的基础上解决重边的问题啦!
实现
与割点的代码特别像。
void DFS(int u,int fa){
id[u]=(++tot),low[u]=id[u];
bool flag=0;
for(auto [v,d]:g[u]){
if(v==fa){
if(flag)low[u]=min(low[u],id[v]);
else flag=1;continue;
}if(id[v])low[u]=min(low[u],id[v]);
else{
DFS(v,u),low[u]=min(low[u],low[v]);
if(low[v]>id[u])ans.pb(d);
}
}return;
}
点双连通分量
介绍
对于某个连通分量,如果任意删去一个点后连通性不变,那么这样的连通分量就被称为点双连通分量。换句话说,如果某个连通分量中不存在任何一个割点,那么该连通分量即为点双连通分量。
点双连通分量也简称点双或 BCC。
做法
有两个重要的性质:
- 两个点双连通分量之间最多只有一个公共点,如果有的话,这个点一定是割点。
- 对于一个点双连通分量,它在 DFS 树中 \(dfn\) 最小的点一定是割点或者根节点。
根据这两个性质,我们可以用栈记录搜索过的点,搜索到割点之后,在回溯时把点双统计出来即可。
实现
注意一个孤点也可以是一个点双连通分量。
void DFS(int u,int fa){
id[u]=(++tot),low[u]=id[u];st.push(u);
for(int v:g[u]){
if(v==fa)continue;
if(id[v])low[u]=min(low[u],id[v]);
else{
DFS(v,u),low[u]=min(low[u],low[v]);
if(low[v]>=id[u]){
vector<int> tmp;tmp.clear();
tmp.pb(u),cnt[u]++;
while(tmp.back()!=v)
tmp.pb(st.top()),cnt[st.top()]++,st.pop();
BCC[++len]=tmp;
}
}
}if(!fa&&cnt[u]==0)BCC[++len]={u};return;
}
例题选讲
D - Yet Another Graph Coloration Problem
首先考虑什么时候会无解。显然,如果原图不连通或者图是一棵树那么一定无解。
任选一个根节点跑 DFS,得到一棵 DFS 树,那么原图一定是这棵树加上若干条反祖边。
对于一条反祖边,我们可以考虑把深度较深的那个点的子树部分全部染黑,子树外的染成白色。那么此时黑白两色之间显然有两条不同路径,一条是直接走 DFS 树的,一条是先走 DFS 树到反祖边一端,通过反祖边到另一端接着再走 DFS 树的。
E - BLO-Blockade
首先如果要被禁止进入的城镇 \(u\) 不是割点,那么造成的后果一定只有 \(2(n-1)\)。
而如果是割点,造成的后果就是 \(\sum_{i=1}^{k} sz_i \times (n-sz_i)\)(\(sz_i\) 为 \(u\) 被割掉后剩下的 \(k\) 个连通块的大小情况)。
在求割点时顺便安装上述分类讨论即可。
F - 矿场搭建
先求出所有割点,然后 DFS 搜索所有连通块,统计出每一个连通块中的割点数量。
然后分类讨论:如果该连通块中没有割点,则至少需要建立两个出口,从任意非割点的地方选择两个点建立即可;如果该连通块中有且仅有一个割点,只需要建立一个出口,任意一个非割点的地方都行;如果有两个及以上个割点,则无需建立,可以直接到达其他联通块。
G - 嗅探器
不难发现,如果 \(u\) 是割点且将 \(A,B\) 两点割在两边,那么 \(u\) 就是可行方案之一。
割点是好找的,难点在于如何判断它是否能把 \(A,B\) 两点割在两边。不难发现我们可以根据 DFS 序判定,如果 \(dfn_u > dfn_A\) 且 \(dfn_u < dfn_B\),或者 \(dfn_u < dfn_A\) 且 \(dfn_u > dfn_B\),那么就是可行的。
但是这样会遗漏,如果 \(A,B\) 都在 \(u\) 子树内,其实也可能是可行的。我们想到用 \(u\) 的某个被割掉的儿子节点 \(v\) 来做判断,如果 \(dfn_v > dfn_A\) 且 \(dfn_v < dfn_B\),或者 \(dfn_v < dfn_A\) 且 \(dfn_v > dfn_B\),那么也是可行的。
H - Substring
题目中有一句至关重要的话:
如果该值可以任意大,输出
-1。
为什么可以任意大?这是怎么回事?发生了什么?答案显而易见——有环呗!
我们采用拓扑排序来判环,然后就只剩一个 DAG 了,上 DP。
定义 \(dp_{i,j}\) 表示以 \(i\) 结尾且出现次数最多的字母为 \(j\) 的路径的最大权值。枚举到一条边 \(u \to v\),则有转移 \(dp_{v,i} = \max(dp_{v,i},dp_{u,i}+[s_v=i])\),其中 \([s_v = i]\) 的方括号是艾佛森括号。
最终答案为 \(\max_{u=1}^{n} \max_{j=1}^{26} dp_{i,j}\)。
I - We Need More Bosses
我们想添加的 boss 所处的边一定是一条割边。割边可以提前找出来。
但是你不能把所有割边都放上 boss,只能选取其中的一条路径。为了最大化 boss 数量,考虑给每条边分配边权,割边的边权为 \(1\) 而普通边的边权为 \(0\)。Tarjan 缩点后求树的直径即可,也可以直接在图上求直径。
总结
割点、割边与点双连通分量,是无向图结构分析的核心内容,借助 Tarjan 算法可在 \(O(n+m)\) 的时间复杂度下高效求解。主要用于判定图的连通稳定性,比如定位删去后会割裂图的关键节点(割点)与边(割边),或是划分无割点的稳定连通子结构(点双连通分量)。其应用场景较广,既能用于拓扑优化,也常结合图论其他问题处理连通性相关的逻辑分析,是处理无向图复杂问题的实用工具。

浙公网安备 33010602011771号