Tarjan算法求割点
求割点要注意的是与求强连通分量的区别:
1、割点对于的是无向图,而非有向图。
2、不需要栈数组
3、low[]的含义变化为:最早能绕到的祖先节点(这里的定义是洛谷题解里面觉得最透彻的,而我觉得应该是:不通过父亲节点,最早能绕到的祖先节点。 下面会有解释~ ),为什么是说绕到,因为是有向图,所以一定会有 儿子–>父亲 这条路,而不能通过这条路到它们的祖先节点(这是因为如果算上这条路的话,儿子 通过这条路,肯定能到达祖先节点,那么low[儿子]的值可能会小于low[父亲],导致low[]数组意义上就是说:如果去掉父亲节点后, 儿子 可以达到 祖先节点,而实际上如果去掉父亲节点,儿子–>父亲 这条路就没有了,儿子是无法到达祖先节点的,所以不能算上这条边!)
4、对于已访问过的节点,必须是low[u]=min(low[u],dfn[v]),而不能是low[u]=min(low[u],low[v])。(后面有细说)
5、判断割点:
①、如果不是根节点,判断它的儿子节点 v ,是否有满足 low[v]>=dfn[u],如果满足,则说明 v 无法绕到 u 的上面节点,而如果去掉 u ,则 v 无法绕到 u 上面节点也就是 v 的祖先节点。
②、对于是根节点,很显然判断这个根节是否有两个子树(子树通过回溯判断,如果有一个子树,则满足这个子树中肯定有一个节点作为子根,然后其他节点连接这个子根节点,之后这个子根再连接根节点)。
对于上面第3、4点,这里提供一个数据:
输入第一行为 n(点数),m(无向边)。
此后则有 m 条无向边。
5 6
1 2
2 3
3 4
4 5
1 3
3 5
我们模拟两种Tarjan算法,一种是low[u] = min( low[u], low[v] );,一种是low[u] = min( low[u], dfn[v] );。
第1种:
① dfs(1),dfn[1] = 1,low[1] = 1。
② dfs(2),dfn[2] = 2,low[2] = 2。
③ dfs(3),dfn[3] = 3,low[3] = 3。
④ 发现回边 3 -> 1,low[3] = 1。
⑤ dfs(4),dfn[4] = 4,low[4] = 4。
⑥ dfs(5),dfn[5] = 5,low[5] = 5。
⑦ 发现回边 5 -> 3,low[5] = 1。
⑧ dfs(5)结束,回到dfs(4),low[4] = 1。
⑨ dfs(4)结束,回到dfs(3),low[3] = 1。
⑩ dfs(3)结束,至此未发现割点。
第2种:
① dfs(1),dfn[1] = 1,low[1] = 1。
② dfs(2),dfn[2] = 2,low[2] = 2。
③ dfs(3),dfn[3] = 3,low[3] = 3。
④ 发现回边 3 -> 1,low[3] = 1。
⑤ dfs(4),dfn[4] = 4,low[4] = 4。
⑥ dfs(5),dfn[5] = 5,low[5] = 5。
⑦ 发现回边 5 -> 3,low[5] = 3。
⑧ dfs(5)结束,回到dfs(4),low[4] = 3。
⑨ dfs(4)结束,回到dfs(3),low[4] >= dfn[3],发现割点3,low[3] = 1。
而这个图中,正确答案是:3是割点。
看法与解释:
1、 问题: 上面我们会发现,第①个是错的,错在 low[5] = 3 而并非等于 1 。而站在low数组方面来讲,明明节点 5 可以不通过 4–> 5 就访问到节点 1 呀,而low定义就是最早能绕到的节点,也就是 1 呀。这么看确实是没错,而对比第②个过程你会发现,对于 3–>4 这条边,由于 low[4]<dfn[3],而无法证明 节点 3 是割点(接下来有说)。
2、 在这里自己的理解是: 由于判断节点 u 是否为割点时,判断的是 low[v] 与 dfn[u] 的关系,是建立在 u–>v 这条边的基础上的。而我们low[]数组的含义应该是:最早绕的祖先节点且不通过父节点! 因为我们判断的是low[v]与dfn[u]的关系,其含义就是如果去掉 父节点 u ,v 能到达祖先节点low[v]。而如果我们low[]定义的是最早绕的祖先节点却通过了父节点,此时会遗漏割点 u 。
在上面的过程①中我们发现:
由于5–>3中,3是已访问的点,low[5]=min(low[5],low[3])=1。
使得在递归返回后,low[4]=min(low[5],low[4])=low[5]=1。
与过程②对比发现,这里会使得low[4]==1,说明4 可以不通过父节点 3 就可以到达 祖先节点 1 ,而在图中是 ** 非法的 ** !这时候导致 low[4]<dfn[3] , 说明 节点 4 可以不通过 3 绕到 3 的祖先节点,所以 3 不是割点,而这是错误的!
综上,必须是与 dfn[] 作对比。
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
#define MAXN 20008
using namespace std;
int n,m;
int cnt,t,son;
int dfn[MAXN],low[MAXN],head[MAXN];
bool vis[MAXN];
struct Edge
{
int to;
int next;
}edge[200008];
inline void add(int u,int v)
{
edge[++cnt].to=v;
edge[cnt].next=head[u];
head[u]=cnt;
return;
}
inline void init()
{
cnt=t=son=0;
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
memset(low,0,sizeof(low));
memset(dfn,0,sizeof(dfn));
return;
}
inline void tarjan(int u,int root)
{
dfn[u]=low[u]=++t;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(!dfn[v]){
tarjan(v,u);
if(u==root) son++;
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=root){
vis[u]=true;
}
}else{
low[u]=min(low[u],dfn[v]);
}
}
if(u==root&&son>1) vis[u]=true;
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
int A,B;
for(int i=1;i<=m;i++){
scanf("%d%d",&A,&B);
add(A,B),add(B,A);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
son=0;
tarjan(i,i);
}
}
int sum=0;
for(int i=1;i<=n;i++){
if(vis[i]){
sum++;
}
}
printf("%d\n",sum);
for(int i=1;i<=n;i++){
if(vis[i]){
printf("%d ",i);
}
}
putchar('\n');
}
}