题解:P3388 【模板】割点(割顶)

算法介绍

Tarjan 算法,这里用来解决割点问题,时间复杂度为 \(O(n+m)\)

割点的简要定义是:去掉割点及其所连的边,该图分为两个及以上的连通分量。

接下来讲的是定义与约定。

\(dfn_i\):称为“时间戳”标记,使用 dfs 第一次遍历到的次序。

搜索树:\(n\) 个点,\(n-1\) 条边搜索生成的树。

\(low_i\):称为“追溯值”,表示的是第 \(i\) 个点不走其父亲的边可以到达的时间戳的最小值。

考虑 \(low_i\) 的计算方法:

  • 自己肯定可达,即 \(low_i=dfn_i\)
  • \(j\)\(i\) 在生成树上的子节点,则取 \(low_i=\min(low_i,low_j)\)
  • \(j\)\(i\) 为非树边,则根据定义,\(low_i=\min(low_i,dfn_j)\)

更新 \(low\) 的伪代码如下:

if y is x's son
  low_x=min(low_x,low_y)
else
  low_x=min(low_x,dfn_y)

如何求割点?

tarjan 算法:

  1. \(x\) 不为根节点,且 \(dfn_x \le low_y\),则 \(x\) 为割点。

由定义知,即不能回到祖先,那么 \(x\) 点是割点。

我们发现,此方法无法用于判断根节点,于是有下面的方法。

  1. \(x\) 为根节点,且 \(x\) 有两颗以上的子树,则 \(x\) 为割点。

非常明显,删去后,整张图就分裂了。

正确性证明

对于第一种割点,我们假设遍历到了 \(x\)\(dfn_x \le low_y\) 表示着表示儿子 \(y\) 点回溯可到达的最先点的时间戳大于等于 \(x\) 的时间戳,所以 \(y\) 点不能通过 \(x\) 点与其他已遍历点相邻,即分裂了。

对于第二种割点,两个儿子必定要通过祖先连接,所以成立。

代码实现

这里也应该讲解一下。

  1. \(dfn\)\(low\) 数组初始全为 \(0\),这点其实也起到标记的作用。
  2. 在遇到没有访问过的节点时,要继续向下搜索。
  3. 每个点都有可能是根节点,因为图不连通。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,cnt,p;
vector<int>v[100005];
int dfn[100005],low[100005];
bool ans[100005];
void tarjan(int x,bool root){
	int p=0;
	dfn[x]=low[x]=++cnt;
	for(int i=0;i<v[x].size();i++){
		int y=v[x][i];
		if(!dfn[y]){
			tarjan(y,0);
			low[x]=min(low[x],low[y]);
			if(low[y]>=dfn[x]&&!root) ans[x]=1;
			if(root) p++;
		}
		else{
			low[x]=min(low[x],dfn[y]);
		}
	}
	if(root&&p>=2) ans[x]=1;
	return;	
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,vv;
		cin>>u>>vv;
		v[u].push_back(vv);
		v[vv].push_back(u);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i,i);
	}
	int h=0;
	for(int i=1;i<=n;i++){
		if(ans[i]) h++;
	}
	cout<<h<<endl;
	for(int i=1;i<=n;i++){
		if(ans[i]) cout<<i<<" ";
	}
	return 0;
}
posted @ 2025-05-26 19:15  easy42  阅读(72)  评论(0)    收藏  举报