双连通分量是无向图中的一个概念,它是指无向图中的一个极大子图,根据限制条件可以分为边双连通分量和点双连通分量,欲了解双连通分量需先了解Tarjan算法,以及割点割边的概念及求解。本篇博客介绍边连通分量的相关内容。
学习边连通分量前,你需要先了解:
关于Tarjan:SCC-Tarjan算法,强连通分量算法,从dfs到Tarjan详解-CSDN博客
关于缩点:SCC-Tarjan,缩点问题-CSDN博客
关于割点:Tarjan-割点问题-CSDN博客
关于割边:Tarjan-割边问题-CSDN博客
在无向图中,存在一个极大子图,其中任意两个顶点之间至少存在两条不同的路径。换句话说,如果从该子图中删除任意一条边,该子图仍然是连通的,我们称该极大子图为边双连通分量(edge Double Connected Components,eDCC)。
无向图中极大的不包含割边的连通分量被称为边双连通分量(edge Double Connected Components,eDCC)。
如下图中的{1,2,3}, {4},{5,6}, {7}, {8}均为eDcc
我们回顾一下Tarjan算法涉及到的概念:
我们dfs对图遍历,保证每个点只访问一次,访问过的节点和边构成一棵有向树,我们称之为搜索树。
如果节点x是某个强连通分量在搜索树中遇到的第一个节点,那么这个强连通分量的其余节点肯定是在搜索树中以x为根的子树中。节点x被称为这个强连通分量的根。
我们用数组dfn[]来保存节点第一次访问时间,dfn[x]即节点x第一次访问的时间戳。
数组low[]来记录每个节点出发能够访问的最早时间戳,记low[x]节点x出发能够访问的最早时间戳,即追溯值。
仍然是基于Tarjan算法进行求解,其实就是Tarjan算法求解割边和强连通分量的结合。
我们Tarjan在有向图求SCC中,通过栈保存连通分量的节点,又通过时间戳和追溯值是否相等来找到强连通分量的根从而从栈中取出节点。
而求解割边时,我们对于low值更新是不允许通过反向边来更新low值的,如通过(x,y)抵达y,但是更新y的low值时不允许通过(y,x)更新。
于是我们将割边求解中的low值更新条件放到Tarjan求解SCC算法中,同时记录割边和连通分量,此时我们可以**保证记录的连通分量为边双连通分量。**因为对于连通分量的根x的割边上的邻点y,由于不允许通过(x,y)更新low[x],所以此时x的low值和dfn值相等,从而在回溯到y之前x以及其栈中上方的节点都已取出,不会导致x和y存入同一个连通分量之中。
如果对于原理还不清楚,可以回顾文章开头前置知识的链接中的内容。
仍然是使用链式前向星存图,关于链式前向星,详见:一种实用的边的存储结构–链式前向星-CSDN博客
#define N 10010
#define M 10010
struct edge
{
int v, nxt;
} edges[M];
int head[N], st[N], edcc[N]{0}, dfn[N], low[N], in[N]{0}, out[N]{0}, idx = 0, top = 0, cnt = 0, tot = 0;
bitset<N> bri;
void addedge(int u, int v)
{
edges[idx] = {v, head[u]};
head[u] = idx++;
}
void tarjan(int x, int pre)
{
dfn[x] = low[x] = ++tot;
st[top++] = x;
int y;
for (int i = head[x]; ~i; i = edges[i].nxt)
{
y = edges[i].v;
if (!dfn[y])
{
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x])
{
bri[i] = bri[i ^ 1] = true;
}
}
else if (i != (pre ^ 1))
{
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x])
{
++cnt;
do
{
y = st[--top];
edcc[y] = cnt;
} while (y != x);
}
}
之前有介绍过强连通分量中的缩点问题(SCC-Tarjan,缩点问题-CSDN博客),自然边双连通分量也可以应用缩点,从而降低图的规模,简化问题。
如果说有向有环图缩点后得到一个有向无环树(森林),那么无向有环图缩点就得到了一个无向无环树(森林)。并且,树(森林)中的树边就是原来的割边。通过观察树(森林),我们重新审视问题从而求解。
下面通过一道OJ题来练习一下刚学会的eDcc求解以及缩点在eDcc上的应用。
为了从 F(1≤F≤5000) 个草场中的一个走到另一个,贝茜和她的同伴们有时不得不路过一些她们讨厌的可怕的树.奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。
每对草场之间已经有至少一条路径.给出所有 R(F ? 1≤ R ≤10000) 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成.两条路径相互分离,是指两条路径没有一条重合的道路.但是,两条分离的路径上可以有一些相同的草场.对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。
[P2860 USACO06JAN] Redundant Paths G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目的主干信息就是给你一个无向图,让你加边,使得任意两点之间之间都至少有两条不同的路径,求最小的加边数。
由于原图中两个点之间可能有两条不同的路,那么对于这两个点一定在同一个eDcc内,因为它们一定处于同一个环中,而环内无割边。
那么我们求解出eDCC之后进行缩点,可以得到一棵无向无环树(题目条件已经保证给的图是连通图了),我们以下图为例
对于树而言,任意两点之间存在唯一一条路径,任意加一条边就会出现回路(树的基本知识)。
那么,如果我们加边得到回路,对于回路内的点自然两两之间有两条路,对于环外的点会发现环伸出环外的边上的两个点之间只有一条路径,但是外面的点与环内的点有两条路径,所以我们贪心的连边,使得树的叶子节点两两连边,会出现这样的情况:
如果叶子节点数目sum为偶数,那么两两配对连了sum/2条边,此时任意两点都至少有两条路径
如果叶子节点数目sum为偶数,那么两两配对最终剩下三个点,我们将三个连两条边一共连接了(sum+1)/2条边,此时任意两点都至少有两条路
如图:
可见答案就是(sum + 1) / 2,如果比这个数字还小,对于奇数叶子会有至少一对点不满足,对于偶数叶子至少两对点不满足。
#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <cmath>
#include <functional>
#include <climits>
#include <bitset>
#include <stack>
#include <cstring>
using namespace std;
#define N 10010
#define M 10010
struct edge
{
int v, nxt;
} edges[M];
int head[N], st[N], edcc[N]{0}, dfn[N], low[N], in[N]{0}, out[N]{0}, idx = 0, top = 0, cnt = 0, tot = 0;
bitset<N> bri;
void addedge(int u, int v)
{
edges[idx] = {v, head[u]};
head[u] = idx++;
}
void tarjan(int x, int pre)
{
dfn[x] = low[x] = ++tot;
st[top++] = x;
int y;
for (int i = head[x]; ~i; i = edges[i].nxt)
{
y = edges[i].v;
if (!dfn[y])
{
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x])
{
bri[i] = bri[i ^ 1] = true;
}
}
else if (i != (pre ^ 1))
{
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x])
{
++cnt;
do
{
y = st[--top];
edcc[y] = cnt;
} while (y != x);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
//freopen("in.txt", "r", stdin);
//freopen("out.txt", "w", stdout);
memset(head, -1, sizeof(head));
int n, m, u, v;
cin >> n >> m;
for (int i = 0; i < m; i++)
{
cin >> u >> v;
addedge(u, v);
addedge(v, u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i, -1);
for (int i = 1; i <= n; i++)
for (int j = head[i]; ~j; j = edges[j].nxt)
{
if (edcc[i] != edcc[edges[j].v])
{
in[edcc[edges[j].v]]++;
out[edcc[i]]++;
}
}
int ans = 0;
for (int i = 1; i <= cnt; i++)
ans += out[i] == 1;
cout << (ans + 1) / 2;
return 0;
}