无向图的双连通分量
若一张无向图不存在割点,则称它为“点双连通图“;若一张无向图不存在桥,则称它为”边双连通图“。
无向图的极大点双连通子图称为“点双连通分量”,简记为“\(\text{V-DCC(Vertex Double Connected Component)}\)”或“点双”;无向图的极大边双连通子图称为“边双连通分量”,简记为“\(\text{E-DCC(Edge Double Connected Component)}\)”或”边双“。二者合称”双连通分量“,简记为”\(\text{DCC(Double Connected Component)}\)“。
需要注意的是,一个孤立点也算是 \(\text{V-DCC}\) 和 \(\text{E-DCC}\)。
V-DCC
如图:
图中 \((5,2,6),(1,3,5),(3,4)\) 是 \(\text{V-DCC}\)。
值得注意的是,一个割点可能属于多个 \(\text{V-DCC}\)。
我们维护一个栈,在 \(\operatorname{dfs}\) 的时候执行以下过程:
- 将当前节点 \(u\) 入栈;
- 当 \(dfn(u)\le low(v)\) 时,我们不需要判断 \(u\) 是否为 \(root\),直接一直弹栈,直到弹出 \(v\),相当于弹出了 \(subtree(v)\),则 \(subtree(v)+u\) 构成一个 \(\text{V-DCC}\)。
#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
using namespace std;
const int MAXN = 5e4 + 5;
const int MAXM = 3e5 + 5;
int cnt, Time, rt, tot;
int head[MAXN], dfn[MAXN], low[MAXN];
stack<int> sta;
vector<int> dcc[MAXN];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++Time;
if (u == rt && !head[u]) //特判孤立点
{
dcc[++tot].push_back(u);
return;
}
sta.push(u);
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v])
{
tot++;
int now = 0;
while (v != now) //弹出 subtree(v)
{
now = sta.top();
sta.pop();
dcc[tot].push_back(now);
}
dcc[tot].push_back(u);
}
}
else
{
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(rt = i);
}
}
for (int i = 1; i <= tot; i++)
{
for (int j = 0; j < dcc[i].size(); j++)
{
printf("%d ", dcc[i][j]);
}
putchar('\n');
}
return 0;
}
E-DCC
显然,桥不可能存在于 \(\text{E-DCC}\) 中,所以我们直接把图中的所有桥都去掉,剩下的连通块就是所有的 \(\text{E-DCC}\) 了。
下图中共 \(3\) 条桥,\(4\) 个 \(\text{E-DCC}\)。
先 \(\rm Tarjan\) 算出所有的桥,再 \(\rm dfs\) 一遍,不访问桥边,这样就能求出每个连通块了。
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 5e4 + 5;
const int MAXM = 3e5 + 5;
int cnt = 1, Time, dcc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool bridge[MAXM << 1];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u, int in_edge)
{
dfn[u] = low[u] = ++Time;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v])
{
bridge[i] = bridge[i ^ 1] = true;
}
}
else if (i != (in_edge ^ 1))
{
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs(int u)
{
c[u] = dcc;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (c[v] || bridge[i]) //忽略一个访问过的和桥边
{
continue;
}
dfs(v);
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i, 0);
}
}
for (int i = 1; i <= n; i++)
{
if (!c[i]) //一个新的连通块
{
dcc++;
dfs(i);
}
}
printf("%d\n", dcc);
return 0;
}
Problem A:连通分量
题意
给一个无向图,询问两点是否属于同一个 \(\text{V/E-DCC}\)(数据保证一个点属于不超过 \(10\) 个点双连通分量) 。
几乎就是模板,需要注意的是由于割点会属于多个 \(\text{V-DCC}\),所以点双的 \(c\) 数组得开成 \(\rm vector\),判断的时候双重循环,由于数据有保证,所以双重循环最多进行 \(10^2=100\) 次,时间复杂度 \(\operatorname{O}(n+100q)\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
using namespace std;
const int MAXN = 3e5 + 5;
const int MAXM = 5e5 + 5;
struct v_dcc
{
int cnt, Time, rt, dcc;
int head[MAXN], dfn[MAXN], low[MAXN];
stack<int> sta;
vector<int> c[MAXN];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++Time;
sta.push(u);;
if (u == rt && !head[u])
{
c[u].push_back(++dcc);
return;
}
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v])
{
dcc++;
int now = 0;
while (v != now)
{
now = sta.top();
sta.pop();
c[now].push_back(dcc);
}
c[u].push_back(dcc);
}
}
else
{
low[u] = min(low[u], dfn[v]);
}
}
}
}cut;
struct e_dcc
{
int cnt = 1, Time, dcc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool bridge[MAXM << 1];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u, int in_edge)
{
dfn[u] = low[u] = ++Time;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v])
{
bridge[i] = bridge[i ^ 1] = true;
}
}
else if (i != (in_edge ^ 1))
{
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs(int u)
{
c[u] = dcc;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (c[v] || bridge[i])
{
continue;
}
dfs(v);
}
}
}bridge;
int main()
{
int n, m, q;
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
cut.add(u, v);
cut.add(v, u);
bridge.add(u, v);
bridge.add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!cut.dfn[i])
{
cut.tarjan(cut.rt = i);
}
}
for (int i = 1; i <= n; i++)
{
if (!bridge.dfn[i])
{
bridge.tarjan(i, 0);
}
}
for (int i = 1; i <= n; i++)
{
if (!bridge.c[i])
{
bridge.dcc++;
bridge.dfs(i);
}
}
while (q--)
{
int op, x, y;
scanf("%d%d%d", &op, &x, &y);
if (op == 1)
{
bool flag = false;
for (int i = 0; i < cut.c[x].size(); i++)
{
for (int j = 0; j < cut.c[y].size(); j++)
{
if (cut.c[x][i] == cut.c[y][j] && !flag)
{
puts("yes");
flag = true;
}
}
}
if (!flag)
{
puts("no");
}
}
else
{
if (bridge.c[x] == bridge.c[y])
{
puts("yes");
}
else
{
puts("no");
}
}
}
return 0;
}
Problem B:多余的路径
P2860 [USACO06JAN]Redundant Paths G
题意
给定一张无向连通图,求最少往图中加入几条边,使得原图变为一个 \(\text{E-DCC}\)。
思路
可以发现同一个 \(\text{E-DCC}\) 内的节点都已经满足要求,所以我们进行缩点,同时统计度数。
缩点后,入度为 \(1\) 的节点一定是叶子节点,我们在两个叶子节点间连一条边即可。
设有 \(k\) 个入度为 \(1\) 的节点,若 \(k\) 是偶数,答案为 \(\frac{k}{2}\);若 \(k\) 为奇数,则我们在 \((k-1)\) 个节点间连边后还有一个节点,它随便连一条边即可。
综上,答案为 \(\left\lceil\frac{k}{2}\right\rceil\)。
Code
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 5e3 + 5;
const int MAXM = 1e4 + 5;
int cnt = 1, Time, dcc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN], du[MAXN];
bool bridge[MAXN];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u, int in_edge)
{
dfn[u] = low[u] = ++Time;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v])
{
bridge[i ^ 1] = bridge[i] = true;
}
}
else if (i != (in_edge ^ 1))
{
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs(int u)
{
c[u] = dcc;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (c[v] || bridge[i])
{
continue;
}
dfs(v);
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i, 0);
}
}
for (int i = 1; i <= n; i++)
{
if (!c[i])
{
dcc++;
dfs(i);
}
}
for (int i = 2; i <= cnt; i += 2)
{
int u = e[i ^ 1].to, v = e[i].to;
if (c[u] != c[v])
{
du[c[u]]++;
du[c[v]]++;
}
}
int ans = 0;
for (int i = 1; i <= dcc; i++)
{
if (du[i] == 1)
{
ans++;
}
}
printf("%d\n", (ans + 1) >> 1);
return 0;
}