20250818 - 割点 割边 总结
前言
会 dfs 序的人都会 tarjan。
概念
连通分量(极大连通子图):极大连通子图就是要使得连通子图的点和边数量尽可能大,注意是极大,不是最大(俗称连通块)
割点:删去某个点后,连通块增加了,那么这个点就是割点
割边:删去某条边后,连通块增加了,那么这个点就是割边
点双:删掉一个点后,连通性不变的联通分量就是点双
割点
方法一:Floyd
枚举中转点不能走,再跑 Floyd 就好了
方法二:dfs爆搜
每个点 dfs 一下即可。
方法三:tarjan
如果对于一个点 \(u\),它至少存在一个儿子 \(v\) 不能回到比 \(u\) 更高的点,那么u就是割点
因为删掉 \(u\) 之后,\(v\) 所在的子树没有返祖边可以连回来。
我们可以用\(low\)和\(dfn\)快速判断是否能连回来,\(low_v\ge dfn_u\) 就表示不能连回来,
只在非根节点时成立
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
const int MAXN = 1e5 + 7;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
void init(){
return;
}
int n,m;
vector<int>e[MAXN];
int dfn[MAXN],low[MAXN],times;
vector<int>ans;
void tarjan(int u,int fa){
dfn[u] = low[u] = ++times;
int son_cnt = 0;
bool flag = false;
for(auto v : e[u]){
if(fa == v) continue;
if(dfn[v]){
low[u] = min(low[u],dfn[v]);
}else{
son_cnt++;
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(!flag && fa != u && low[v] >= dfn[u]){
flag = 1;
ans.push_back(u);
}
}
}
if(fa == u && son_cnt > 1)
ans.push_back(u);
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d",&x,&y);
e[x].push_back(y);
e[y].push_back(x);
}
for(int i = 1;i <= n;i++){
if(!dfn[i]){
tarjan(i,i);
}
}
sort(ans.begin(),ans.end());
printf("%d\n",ans.size());
for(auto y : ans)
printf("%d ",y);
return 0;
}
割边
如果对于一个点 \(u\),它至少存在一个儿子 \(v\) 不能回到比 \(u\) 更高的点,那么u就是割边
因为删掉 \(u\) 之后,\(v\) 所在的子树没有返祖边可以连回来。
我们可以用\(low\)和\(dfn\)快速判断是否能连回来,\(low_v > dfn_u\) 就表示不能连回来,
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
const int MAXN = 1e5 + 7;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
void init(){
return;
}
int n,m;
vector<int>e[MAXN];
int dfn[MAXN],low[MAXN],times;
vector<pair<int,int>>ans;
bool cmp(const pair<int,int> &x,const pair<int,int> &y){
if(x.first != y.first)
return x.first < y.first;
return x.second < y.second;
}
void tarjan(int u,int fa){
dfn[u] = low[u] = ++times;
for(auto v : e[u]){
if(v == fa) continue;
if(dfn[v]){
low[u] = min(dfn[v],low[u]);
}else{
tarjan(v,u);
low[u] = min(low[v],low[u]);
if(low[v] > dfn[u]){
ans.push_back({min(u,v),max(u,v)});
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d",&x,&y);
e[x].push_back(y);
e[y].push_back(x);
}
for(int i = 1;i <= n;i++){
if(!dfn[i])
tarjan(i,i);
}
sort(ans.begin(),ans.end(),cmp);
for(auto y : ans){
printf("%d %d\n",y.first,y.second);
}
return 0;
}
点双
删去一个点后连通性不变的连通分量就是点双连通分量,简称点双。
- 两个点双之间最多只有一个公共点,这个点是割点。
- 对于一个点双,它在搜索树中dfn最小的点一定是割点或根节点
所以,我们用栈记录搜索过的点,在搜索到割点之后的回溯段,把点双统计出来即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
const int MAXN = 5e5 + 7;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
void init(){
return;
}
int n,m;
vector<int>e[MAXN];
stack<int>st;
int dfn[MAXN],low[MAXN],times,deg[MAXN];
vector<vector<int>>ans;
void tarjan(int u,int fa){
st.push(u);
dfn[u] = low[u] = ++times;
for(auto v : e[u]){
if(fa == v) continue;
if(dfn[v]){
low[u] = min(low[u],dfn[v]);
}else{
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(low[v] >= dfn[u]){
// printf("???%d %d\n",u,v);
vector<int>tmp{u};
while(tmp.back() != v){
tmp.push_back(st.top());
deg[st.top()]++;
st.pop();
}
deg[u]++;
ans.push_back(tmp);
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d",&x,&y);
e[x].push_back(y);
e[y].push_back(x);
}
for(int i = 1;i <= n;i++){
if(!dfn[i]){
tarjan(i,i);
// while(st.size()) st.pop();
}
}
// for(int i = 1;i <= n;i++){
// printf("%d ",dfn[i]);
// }
// printf("\n");
// for(int i = 1;i <= n;i++){
// printf("%d ",low[i]);
// }
// printf("\n");
for(int i = 1;i <= n;i++){
if(!deg[i]){
ans.push_back(vector<int>(1,i));
}
}
printf("%d\n",ans.size());
for(int i = 0;i < ans.size();i++){
printf("%d ",ans[i].size());
for(auto y : ans[i]){
printf("%d ",y);
}
printf("\n");
}
return 0;
}