题解: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 算法:
- 若 \(x\) 不为根节点,且 \(dfn_x \le low_y\),则 \(x\) 为割点。
由定义知,即不能回到祖先,那么 \(x\) 点是割点。
我们发现,此方法无法用于判断根节点,于是有下面的方法。
- 若 \(x\) 为根节点,且 \(x\) 有两颗以上的子树,则 \(x\) 为割点。
非常明显,删去后,整张图就分裂了。
正确性证明
对于第一种割点,我们假设遍历到了 \(x\),\(dfn_x \le low_y\) 表示着表示儿子 \(y\) 点回溯可到达的最先点的时间戳大于等于 \(x\) 的时间戳,所以 \(y\) 点不能通过 \(x\) 点与其他已遍历点相邻,即分裂了。
对于第二种割点,两个儿子必定要通过祖先连接,所以成立。
代码实现
这里也应该讲解一下。
- \(dfn\) 和 \(low\) 数组初始全为 \(0\),这点其实也起到标记的作用。
- 在遇到没有访问过的节点时,要继续向下搜索。
- 每个点都有可能是根节点,因为图不连通。
#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;
}

浙公网安备 33010602011771号