【题目链接】
【题目考点】
1. 割点,点双连通分量
在学习本节前需要先学习"tarjan求强连通分量"一节,参考:【模板:强连通分量】信息学奥赛一本通 1516:消息的传递
(1) 割点及点双连通概念
割点 :无向图中,如果删掉掉一个顶点及与它相邻的所有边后,图中连通分量数量增多,则称该顶点为割点(或关节点)。
对于一个无向连通图:
如果不存在割点,则称它为点双连通图 。
点双连通图中:任意两条边都同在一个简单环中。除特殊情况,任意两顶点间至少存在两条顶点不重复路径。
点双连通的极大子图称为点双连通分量
(2) tarjan算法求无向图的割点及点双连通分量
无向图的DFS生成树中没有横叉边、前向边
dfn[i]:顶点i的时间戳,表示顶点i的搜索次序编号,即顶点i是第几个被访问的顶点。
low[i]:顶点i的追溯值,表示顶点i不通过父结点可以回溯到的第一个祖先结点的时间戳,也是当前顶点i所在的表示一个点双连通分量的子树的根结点的时间戳。
tarjan算法描述如下:
- 尝试从每个顶点开始进行深搜
- 首次访问顶点u时设初值
dfn[u]=low[u]=++ts,ts是从0开始不断增大的时间戳变量。 - 访问u的所有邻接点,其中v是u的一个邻接点:
- 如果未访问过v,从v开始深搜。深搜结束后回溯时更新:
low[u] = min(low[u], low[v]) - 如果u满足割点的条件:
(1)如果u是根结点,且有至少2个孩子,或者
(2)u不是根结点,v是某个u的孩子,满足dfn[u] <= low[v]。
则将u加入割点集合。 - 如果已访问过v,那么更新
low[u] = min(low[u], dfn[v])
- 如果未访问过v,从v开始深搜。深搜结束后回溯时更新:
最后输出割点集合中的顶点,即为该图的割点。
如果想求点双连通分量,需要在访问顶点u时将顶点u入栈
在深搜回溯后判断u是否为割点的位置做如下操作:
如果满足dfn[u] <= low[v],将u加入集合,不断出栈并将出栈元素加入集合,直到v出栈。该集合中的顶点构成一个点双连通分量。
【解题思路】
本题为模板题,根据上述描述实现tarjan算法。
由于一个顶点可能会被反复求出是割点,因此对求出的割点要做"去重"操作。
可以使用STL中的set类保存求出的割点,也可以使用bool类型的标记数组来标记哪个顶点是割点。
【题解代码】
解法1:tarjan算法求割点
- 写法1:使用set保存割点
cpp
#include <bits/stdc++.h>
using namespace std;
#define N 20005
int n, m;
vector<int> edge[N];
int dfn[N], low[N], ts;
set<int> cutVer;//保存割点
void tarjan(int u, int root)//root:当前访问dfs生成树的根结点
{
int child = 0;
dfn[u] = low[u] = ++ts;
for(int v : edge[u])
{
if(dfn[v] == 0)
{
tarjan(v, root);
low[u] = min(low[u], low[v]);
if(u == root && ++child > 1 || u != root && dfn[u] <= low[v])
cutVer.insert(u);//将u添加到割点集合中(根结点可能会被多次加入,通过set去重)
}
else
low[u] = min(low[u], dfn[v]);
}
}
int main()
{
int f, t;
cin >> n >> m;
for(int i = 1; i <= m; ++i)
{
cin >> f >> t;
edge[f].push_back(t);
edge[t].push_back(f);
}
for(int v = 1; v <= n; ++v)
if(dfn[v] == 0)
tarjan(v, v);
cout << cutVer.size() << endl;
for(int v : cutVer)
cout << v << ' ';
return 0;
}
- 写法2:使用标记数组保存割点
cpp
#include <bits/stdc++.h>
using namespace std;
#define N 20005
int n, m;
vector<int> edge[N];
int dfn[N], low[N], ts, cutVerNum;
bool cutVer[N];//保存割点
void tarjan(int u, int root)//root:当前访问dfs生成树的根结点
{
int child = 0;
dfn[u] = low[u] = ++ts;
for(int v : edge[u])
{
if(dfn[v] == 0)
{
tarjan(v, root);
low[u] = min(low[u], low[v]);
if(u == root && ++child > 1 || u != root && dfn[u] <= low[v])
cutVer[u] = true;//将u添加到割点集合中,一个结点可能会被多次加入
}
else
low[u] = min(low[u], dfn[v]);
}
}
int main()
{
int f, t;
cin >> n >> m;
for(int i = 1; i <= m; ++i)
{
cin >> f >> t;
edge[f].push_back(t);
edge[t].push_back(f);
}
for(int v = 1; v <= n; ++v)
if(dfn[v] == 0)
tarjan(v, v);
for(int i = 1; i <= n; ++i)
if(cutVer[i])
cutVerNum++;
cout << cutVerNum << endl;
for(int i = 1; i <= n; ++i) if(cutVer[i])
cout << i << ' ';
return 0;
}