目录
无向图的双连通分量
概念
对于有向图只有一种强连通分量,而无向图有两种双连通分量。
①边双连通分量 e-DCC
桥:
桥是一条这样的边:对于一个连通的无向图,删除桥会使得无向图不连通。
如此即可定义e-DCC:无向图中极大的不包含桥的连通块即为边双连通分量
易得,对于e-DCC,删除其中的任意一条边,整个无向图仍然是连通的。e-DCC中任意两点间存在两条没有公共边的路径,或任意两点间存在两条互相分离的路径。
②点双连通分量 v-DCC
割点:
割点是一个这样的点:对于一个连通的无向图,删除割点及其所有连边会使得无向图不连通。
如此即可定义v-DCC:无向图中极大的不包含割点的连通块即为点双连通分量
显然,割点至少属于两个连通分量,删除割点一定至少产生两个连通块:若删除割点只产生一个连通块,则删除割点并没有使得图不连通,这就不是割点。
割点与桥之间并没有明显关系,两个割点的连边不一定是桥,桥的两端也不一定是割点。
e-DCC Tarjan算法
dfn、low定义同有向图的强连通分量
对于无向图,不存在横叉边的概念,即不存在未遍历的可到达已遍历的点的边,因为已遍历的点一定会遍历这条边,因为这条边是无向边。
判断当前边是否为桥:
根据桥的性质,桥是桥两边连通分量的唯一连边,桥(x, y)严格满足dfn[x] < low[y],即按照时间戳的顺序,y不能遍历到x或x的祖先节点。某点最早只能遍历自身,则该点是桥的一端。该点的满足dfn[x] < low[y] 的邻点x为桥的另一端。
同时,对于点y,满足dfn[y] == low[y],因为y是桥的一端,则y能遍历到的点为一个e-DCC。
寻找e-DCC:
方法①删除所有桥
方法②类似有向图寻找强连通分量,使用栈。对于dfn[x] == low[x] 的点x,是桥的一端,以x开始能够遍历到的点为e-DCC。tarjan算法
对于e-DCC:连通块是e-DCC 是 连通块中任意两点间均存在互相分离的两条路径 的 充要条件。
若求使无向图全图为e-DCC需要加多少边,则首先求出当前图中的e-DCC、缩点,易得当前图中没有桥。遍历当前图中点,记录度数为1的点,记作cnt,则需要cnt / 2(向上取整)条边。
例题
Acwing395
根据上述分析,本题的思路是建图、缩点、判断桥;易得度数为1的点只有可能出现在桥的一端,当然可以不判断桥遍历所有点判断度数,此处是省时间的做法,遍历桥,为桥的出边端点度数加1,然后遍历缩点后的所有点,记录度数为1的点的个数,输出结果。
细节详见代码。
代码
tarjan算法
void tarjan(int u, int from)//无向图tarjan算法多一个参数from指u的来边,由于无向图的一条边本质上是两条相反有向边,应当防止遍历到反向边。
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i); //当前点为j,来边为i
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j]) //由上述分析可知,若存在u的邻点j能遍历的最早的点晚于u,则认为j不可到u,即u到j的出边i是桥
{
is_bridge[i] = is_bridge[i ^ 1] = true; //i^1是由于建图时正反两边总是依次加入,二者所表示的无向边为桥
}
}
else if (i != (from ^ 1)) //若当前边i不是u的来边的反向边,则可遍历点j,更新low[u]
{
low[u] = min(low[u], dfn[j]);
}
}
if (dfn[u] == low[u])
{
++edcc_cnt;
int y;
do
{
y = stk[top--];
id[y] = edcc_cnt;
} while (y != u);
}
}
AC代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <unordered_map>
#include <cmath>
#include <queue>
#include <string>
#include <cstring>
#include <cstdio>
#include <vector>
#include <map>
#include <iomanip>
#include <algorithm>
using namespace std;
#define ll long long
#define PII pair<int, int>
#define cao ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
const int N = 5010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], edcc_cnt;
bool is_bridge[M];
int d[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u, int from)//无向图tarjan算法多一个参数from指u的来边,由于无向图的一条边本质上是两条相反有向边,应当防止遍历到反向边。
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i); //当前点为j,来边为i
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j]) //由上述分析可知,若存在u的邻点j能遍历的最早的点晚于u,则认为j不可到u,即u到j的出边i是桥
{
is_bridge[i] = is_bridge[i ^ 1] = true; //i^1是由于建图时正反两边总是依次加入,二者所表示的无向边为桥
}
}
else if (i != (from ^ 1)) //若当前边i不是u的来边的反向边,则可遍历点j,更新low[u]
{
low[u] = min(low[u], dfn[j]);
}
}
if (dfn[u] == low[u])
{
++edcc_cnt;
int y;
do
{
y = stk[top--];
id[y] = edcc_cnt;
} while (y != u);
}
}
int main() {
cao;
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b); //正反加两次
add(b, a);
}
tarjan(1, -1); //题目所描述的无向图是连通的,所以对于无向图只需要执行一次,而有向图无论连通与否都要遍历所有点执行
//方法一:判桥,为桥的出边端点度数+1
/*for (int i = 0; i < idx; i++)
{
if (is_bridge[i])
{
d[id[e[i]]]++;
}
}*/
//方法二:不判桥,相应地tarjan算法中无需判桥,遍历所有边,为每条边的出边端点度数+1
//由于无向图存图机制,一条无向边实际上是两条有向边,若为每条边的两端点度数都+1,则最终两端点度数实际上+2.
/*for (int i = 1; i <= n; i++)
{
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b)
{
//d[a]++;
d[b]++;
}
}
}*/
//运行代码时方法一、二应当解除其中一个注释。
int res = 0;
for (int i = 1; i <= edcc_cnt; i++)
{
if (d[i] == 1)
{
res++;
}
}
cout << (res + 1 >> 1) << endl;
return 0;
}
v-DCC Tarjan算法
判断当前点是否为割点
对于一条边的两端点x, y,y不能先行遍历到x或x之前的点,即low[y] >= dfn[x],这里可以等于,是因为对于割点,x如果是y能搜索到的最早的节点,删除割点x时与y相连的边也被删除,同样使得连通块之间不连通。而对于e-DCC,只删除一条边的情况下y与x仍然相连,则边(x, y)不是桥。
①如果x不是根节点,满足low[y] >= dfn[x],则x是割点
②如果x是根节点,x至少要有两个子节点才可以是割点。即满足low[yi] >= dfn[x];
寻找割点:
记录cnt为割点个数,root为当前搜索树的根节点,若未知原图是否连通,则遍历时应当确定当前搜索树的根节点,即调用tarjan的第一个点。
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (low[j] >= dfn[u])
{
cnt++;
}
}
else
{
low[u] = min(low[u], dfn[j]);
}
}
if (u != root && cnt)
{
cnt++;
}
}
寻找v-DCC:
在寻找割点的基础上,搜索时将搜索到的点入栈,当遇到满足割点条件的点时,满足以下两点条件栈中元素为一个v-DCC:
①满足条件的点不是根节点②若满足条件的点是根节点,则要求统计的当前v-DCC数量应当大于1,即当前搜索到的满足条件的双连通分量不应当是第一个,即满足根节点为割点的条件是根节点应当至少有两个子节点或双连通分量。
对于条件②:若当前图中以根节点为割点分离出一个v-DCC,则根节点与这一双连通分量必然相连,则根节点不是割点。
平凡图、孤立点也是v-DCC。
关于割点判断中dfn[x] == low[y]的补充说明:可以发现当二者严格相等时,x也属于这一连通分量。那么二者不能相同时x仍属于这一连通分量吗?
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u;
if (u == root && h[u] == -1)//孤立点,是当前搜索树根节点且无邻点
{
vdcc_cnt++;
vdcc[vdcc_cnt].push_back(u);
return;
}
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (dfn[u] <= low[j])
{
cnt++; //记录割点数量
if (u != root || cnt > 1) //根节点不能被判断为第一个割点
{
is_cut[u] = true;
}
vdcc_cnt++;
int y;
do
{
y = stk[top--];
vdcc[vdcc_cnt].push_back(y);
} while (y != j);//u会在下一行加入,详见上面解释。
vdcc[vdcc_cnt].push_back(u);
}
}
else
{
low[u] = min(low[u], dfn[j]);
}
}
}
例题
Acwing396
代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <unordered_map>
#include <cmath>
#include <queue>
#include <string>
#include <cstring>
#include <cstdio>
#include <vector>
#include <map>
#include <iomanip>
#include <algorithm>
using namespace std;
#define ll unsigned long long
#define PII pair<int, int>
#define cao ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
const int N = 1010, M = 1010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int vdcc_cnt, root;
vector<int> vdcc[N];
bool is_cut[N];
int _;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u;
if (u == root && h[u] == -1)//孤立点,是当前搜索树根节点且无邻点
{
vdcc_cnt++;
vdcc[vdcc_cnt].push_back(u);
return;
}
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (dfn[u] <= low[j])
{
cnt++;
if (u != root || cnt > 1)
{
is_cut[u] = true;
}
vdcc_cnt++;
int y;
do
{
y = stk[top--];
vdcc[vdcc_cnt].push_back(y);
} while (y != j);
vdcc[vdcc_cnt].push_back(u);
}
}
else
{
low[u] = min(low[u], dfn[j]);
}
}
}
int main() {
int _ = 1;
while (cin >> m, m)
{
;
if (!m)
{
break;
}
for (int i = 1; i <= vdcc_cnt; i++)
{
vdcc[i].clear();
}
top = vdcc_cnt = idx = n = timestamp = 0;
memset(dfn, 0, sizeof dfn);
memset(h, -1, sizeof h);
memset(is_cut, 0, sizeof is_cut);
while (m--)
{
int a, b;
cin >> a >> b;
n = max(n, a);
n = max(b, n);
add(a, b);
add(b, a);
}
for (root = 1; root <= n; root++)
{
if (!dfn[root])
{
tarjan(root);
}
}
int res = 0;
ll num = 1;
for (int i = 1; i <= vdcc_cnt; i++)
{
int cnt = 0;
for (int j = 0; j < vdcc[i].size(); j++)
{
if (is_cut[vdcc[i][j]])
{
cnt++;
}
}
if (!cnt)
{
if (vdcc[i].size() <= 1)
{
res++;
}
else
{
res += 2;
num *= vdcc[i].size() * (vdcc[i].size() - 1) / 2;
}
}
else if (cnt == 1)
{
res++;
num *= vdcc[i].size() - 1;
}
}
printf("Case %d: %d %llu\n", _++, res, num);
}
return 0;
}