P3388 【模板】割点(割顶)
题目背景
割点
题目描述
给出一个(n)个点,(m)条边的无向图,求图的割点。
输入输出格式
输入格式:
第一行输入(n,m)
下面(m)行每行输入(x,y)表示(x)到(y)有一条边
输出格式:
第一行输出割点个数
第二行按照节点编号从小到大输出节点,用空格隔开
说明
(n,m)均为100000
(tarjan)图不一定联通!!!
割点:在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。
以上是广义一点的定义,对这个题,要求的为求解所有联通子块的割点,且这个割点集合仅有一个元素。
首先我们明确割点的几个性质:(在一个连通图中)
- 若点(u)是一个割点,则至少存在一对点(v_1,v_2),使得(v_1,v_2)的任何一个路径通过点(u)。
- 对于一颗树,若某个点的入度大于1,则这个点为割点。
有了以上两个性质,我们可以借助(tarjan)求强连通分量的思想求解割点了。
首先,我们同样定义(dfn)和(low)数组,分别代表 遍历到某点的时间 和 某点不通过它父亲下能到达的最小时间
现在开始利用性质1,对于图中某父节点(u)((u)不为遍历开始的点)和其子节点(v),在刚进入子节点时,父亲的(dfn)是一定大于子节点的,我们遍历子节点不通过(u)能够到达的边,并用最小值更新(low)数组。遍历结束后,若(low[v]>=dfn[u])即点(v)不通过连接它父亲的边是不能到达它父亲上面的点的(即时间戳小于(dfn[u])的点),那么点(u)即满足了性质1,成为了割点。
对于父节点(u)为遍历使点的时候,我们将连通图(G)看做是一颗树(当存在(u)为某环上的点时,等效为断开与(u)相连的某一条边,因为我们此时本来就是在探讨(u)是否为割点,所以断开其实不影响判断),此时,若这个(u)的入(出)度大于1,则(u)为割点。(其实这个模拟一下也是可以想明白的)
最后说一个问题:在某点更新(low)时,若用来更新的点不能进去(即已经进去),一定要写成(low[now]=min(low[now],dfn[v])),而不是(low[now]=min(low[now],low[v]);)
为什么?考虑这样一种情况,若点(i)恰好可以到达自己的父亲(u)(不通过父亲找到它的那条边),按照性质1,它父亲(u)仍然是割点。但是,如何它父亲(u)先经过某条边把自己的(low)更新小了,岂不是就错了吗,可以看看这个例子模拟理解一下。
按1,2,3,3,4,5顺序遍历即可
code:
#include <cstdio>
int min(int x,int y){return x<y?x:y;}
const int N=100010;
struct Edge
{
int to,next;
}edge[N<<1];
int head[N],cnt=0,n,m;
void add(int u,int v)
{
edge[++cnt].next=head[u];edge[cnt].to=v;head[u]=cnt;
}
int time=0,dfn[N],low[N],ans[N];
void tarjan(int now,int fa)
{
int child=0;
dfn[now]=low[now]=++time;
for(int i=head[now];i;i=edge[i].next)
{
int v=edge[i].to;
if(!dfn[v])
{
tarjan(v,fa);
if(low[v]>=dfn[now]&&now!=fa)
ans[now]=1;
if(now==fa)
child++;
low[now]=min(low[now],low[v]);
}
low[now]=min(low[now],dfn[v]);
}
if(now==fa&&child>1) ans[now]=1;
}
int main()
{
scanf("%d%d",&n,&m);
int u,v;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i,i);
int cntt=0;
for(int i=1;i<=n;i++)
if(ans[i]) cntt++;
printf("%d
",cntt);
for(int i=1;i<=n;i++)
if(ans[i]) printf("%d ",i);
return 0;
}
2018.6.7