题目链接(You can click it.)
题目含义:
有一张无向图,在其中添边。问每次添边之后,还剩多少桥。
一种较为简单的解法(不涉及e_DCC缩点)(You can click it.)
接下来介绍一个设计e_DCC缩点的思路。e_DCC缩点其实就是将一个边双连通分量看作是一个点,所以需要求出所有的边双连通分量。缩点的操作如下:
- 求桥(tarjan)
- 求不含桥边的连通块(每一个连通块就是一个e_DCC边双连通分量了)
- 将每一个连通块当作一个点处理
然后当然还要重新建图了(要不然缩点干嘛),重新建图的时候就遍历所有的边,如果每个边的两个端点不在同一个边双连通分量,那么他们的连边就是割边,e_DCC缩点的新图中,边都是原图中的割边,所以直接将两个点所在的边双连通分量连边即可。
然后对新图,求每个点(边双连通分量)的深度(求LCA要用),初始状态的桥的数量为缩点后的点数量-1(有n个点显然就是n-1个边了,因为缩点后是不会有环的,而且新图的边全部都是桥),对于每次操作,如果两个点在同一个边双连通分量,显然操作是无效的,如果不在同一个边双连通分量,就求一下两个点对应的新点(就是两个点分别在哪个边双连通分量中),求两个新点的LCA,并且在想上返回的图中,把遇到的桥边标记为非桥边(因为操作后它显然已经不是桥边了),然后就是每遇到这么一个边, a n s = a n s − 1 ans = ans - 1 ans=ans−1即可。
也许你也注意到了,我们还要将每个新点的父节点求出来(方便向上返回,可以在求深度的时候顺便求出),还需要求出桥边(因为是新图,其实每个边都是桥边,完全可以不考虑这个,想了想还是提一下吧),用一个数组 m a r k [ i ] mark[i] mark[i]的值表示 i i i到父节点的连边是否为桥边,求深度都时候加上标记就行了。
/**
* Author : correct
*/
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
#define mem(a, b) memset(a, b, sizeof a)
const int N = 100100, M = 400100;
int n, m, K;
int head[N], nex[M], to[M], cnt;
int head1[N], nex1[M], to1[M], cnt1;
int dfn[N], low[N], num;
int e_DCC[N], dcc;
bool bridge[M];
int fa[N], d[N];
bool mark[N];
void init(){
mem(head, 0), mem(nex, 0), cnt = 1;
mem(head1, 0), mem(nex1, 0), cnt1 = 1;
mem(dfn, 0), mem(low, 0), mem(e_DCC, 0), num = 0, dcc = 0, mem(bridge, 0);
mem(fa, -1), mem(d, 0), mem(mark, 0);
}
void add(int a, int b){
++cnt, to[cnt] = b, nex[cnt] = head[a], head[a] = cnt;
}
void add1(int a, int b){
++cnt1, to1[cnt1] = b, nex1[cnt1] = head1[a], head1[a] = cnt1;
}
void tarjan(int x, int ind){
dfn[x] = low[x] = ++num;
for (int i = head[x]; i; i = nex[i]){
int y = to[i];
if (!dfn[y]){
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (dfn[x] < low[y]){
bridge[i] = bridge[i ^ 1] = 1;
}
}
else if (i != (ind ^ 1))low[x] = min(low[x], low[y]);
}
}
void dfs(int x){
e_DCC[x] = dcc;
for (int i = head[x]; i; i = nex[i]){
int y = to[i];
if (bridge[i] || e_DCC[y])continue;
dfs(y);
}
}
void DFS(int x, int depth){
d[x] = depth;
for (int i = head1[x]; i; i = nex1[i]){
int y = to1[i];
if (!d[y]){
DFS(y, depth + 1);
fa[y] = x;
mark[y] = 1;
// 新图中的所有边一开始都是割边,因此可以不考虑父节点的割边初始化的问题
// 可以将mark[y] = 1;注释
// 在初始化函数init中直接将mark数组初始化为全是割边(全是1)即可
}
}
}
int main()
{
// freopen("in.in", "r", stdin);
while (~scanf("%d %d", &n, &m) && n && m){
init();
for (int i = 0; i < m; i++){
int a, b;
scanf("%d %d", &a, &b);
add(a, b), add(b, a);
}
for (int i = 1; i <= n; i++){
// 求桥
if (!dfn[i]){
tarjan(i, 0);
}
}
for (int i = 1; i <= n; i++){
// 求连通块
if (!e_DCC[i]){
++dcc;
dfs(i);
}
}
for (int i = 2; i <= cnt; i++){
// 建新图
int x, y;
x = to[i], y = to[i ^ 1];
if (e_DCC[x] != e_DCC[y]){
add1(e_DCC[x], e_DCC[y]);
}
}
for (int i = 1; i <= dcc; i++){
// 求深度
if (!d[i]){
DFS(i, 1);
}
}
printf("Case %d:\n", ++K);
int ans = dcc - 1;
int Q;
scanf("%d", &Q);
while (Q--){
int a, b;
scanf("%d %d", &a, &b);
if (e_DCC[a] != e_DCC[b]){
a = e_DCC[a];
b = e_DCC[b];
// 以下为求LCA部分
if (d[a] < d[b]){
a = a ^ b, b = a ^ b, a = a ^ b;
}
while (d[a] > d[b]){
if (mark[a]){
ans--;
mark[a] = 0;
}
a = fa[a];
}
while (a != b){
if (mark[a]){
ans--;
mark[a] = 0;
}
if (mark[b]){
ans--;
mark[b] = 0;
}
a = fa[a];
b = fa[b];
}
}
printf("%d\n", ans);
}
puts("");
}
return 0;
}
当然,求 lca 还可以采用倍增的方式求解,但是因为我们需要对从当前点到 lca 的所有的边都遍历一下(向上扫描),因此求无论如何都需要扫描一遍,就不采用倍增的方式了。
但是还可以优化,写在另一篇博客里面吧。