题目链接(You can click it.)
题目含义:
有一张无向图,在其中添边。问每次添边之后,还剩多少桥。
前面两篇博文已经说的很明了了吧QAQ,本文说的是带并查集优化解法。
首先想一下有什么可以优化的空间。在求出两个点的 lca 后,需要将两个点向上遍历到 lca 处,并且把路径上的桥边标记为非桥边,但是我们发现, 如果这个路径上本来就有一段路径被标记为了非桥边 ,再次走一遍就重复了,能否去掉重复呢?
答案是肯定的,可以去掉重复。(也就是并查集优化啦~~~)
我们可以做这么一个处理,如果一个点的父路径被标记为了非桥边,就跳过这条边,继续往上(和暴力的向上搜索一样,只是把这个边跳过了),如何跳过呢?利用并查集的思想, 直接将这个点并到它的祖先节点中第一个桥边的位置即可 ,这样就可以遇到跳过非桥边,并且从第一个桥边继续向上搜索。
初始时刻,数组 r o o t [ N ] root[N] root[N]的每个值都是 r o o t [ i ] = i root[i] = i root[i]=i,即每个点向上走的第一个桥边就是到父节点的边,一旦经过了一条桥边并将其置为非桥边,就将 r o o t root root的值修改,如果 x x x是子节点, y y y是父节点,那么就是 r o o t [ x ] = y root[x]=y root[x]=y,或者 r o o t [ x ] = r o o t [ y ] root[x]=root[y] root[x]=root[y],或 r o o t [ x ] = f i n d ( y ) root[x] = find(y) root[x]=find(y), f i n d find find是并查集中的查询函数。
除此以外还需要一些小更改,判断是否到达 lca 点不能再用等于了,因为有可能路径压缩后,直接指向了一个 lca 的祖先节点,因此需要判断深度,如果深度小于或等于 lca 了,说明到 lca 了。然后就是每次向上搜索到父节点时,都需要执行一次并查集中的查询函数,查询自身的根节点,也就是可以一下子最多走到哪里,因为我们路径压缩使得图中只剩下了桥边,并且初始时刻,缩点建图后图中也只有桥边,因此不用判断经过的边是否为桥(一定是桥边了),直接就可以进行 a n s = a n s − 1 ans = ans - 1 ans=ans−1(因为此时它是桥边,而后变为非桥边了)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define mem(a, b) memset(a, b, sizeof a)
using namespace std;
const int N = 100100, M = 400100;
int head[N], to[M], nex[M], cnt;
int head1[N], to1[M], nex1[M], cnt1;
int dfn[N], low[N], num;
int e_DCC[N], dcc;
int f[N][20], d[N], root[N];
bool bridge[M];
int n, m;
void init(){
mem(head, 0), mem(nex, 0), cnt = 1;
mem(head1, 0), mem(nex1, 0), cnt1 = 1;
mem(dfn, 0), mem(low, 0), num = 0;
mem(e_DCC, 0), dcc = 0;
mem(f, -1), mem(bridge, 0), mem(d, 0);
for (int i = 0; i < N; i++)root[i] = i;
}
void add(int x, int y){
to[++cnt] = y;
nex[cnt] = head[x];
head[x] = cnt;
}
void add1(int x, int y){
to1[++cnt1] = y;
nex1[cnt1] = head1[x];
head1[x] = cnt1;
}
void tarjan(int x, int ind){
// tarjan求桥
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 (ind != (i ^ 1))low[x] = min(low[x], dfn[y]);
}
}
void dfs(int x){
// dfs求连通块(求边双连通分量)
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){
// dfs求每个边双连通分量的深度(将每个边双连通分量看做一个点)
d[x] = depth;
for (int i = head1[x]; i; i = nex1[i]){
int y = to1[i];
if (!d[y]){
DFS(y, depth + 1);
f[y][0] = x;
}
}
}
int lca(int a, int b){
// 求LCA
if (d[a] < d[b]){
a = a ^ b, b = a ^ b, a = a ^ b;
}
for (int i = 18; i >= 0; i--){
if (d[f[a][i]] >= d[b])a = f[a][i];
}
if (a == b)return a;
for (int i = 18; i >= 0; i--){
if (f[a][i] != f[b][i]){
a = f[a][i], b = f[b][i];
}
}
return f[a][0];
}
int find(int a){
// 并查集 查询函数
return a == root[a] ? a : root[a] = find(root[a]);
}
int main()
{
int k = 0;
while (~scanf("%d %d", &n, &m) && n && m){
init();
while (m--){
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);
}
}
int ans = dcc - 1;
for (int i = 2; i <= cnt; i++){
// 缩点 构建新图
int x = to[i];
int 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);
}
for (int i = 1; i <= 18; i++){
// 预处理数组f 求LCA备用
for (int j = 1; j <= dcc; j++){
f[j][i] = f[f[j][i - 1]][i - 1];
}
}
printf("Case %d:\n", ++k);
int Q;
cin >> Q;
while (Q--){
int x, y;
scanf("%d %d", &x, &y);
x = e_DCC[x], y = e_DCC[y];
if (x != y){
int lc = lca(x, y);
x = find(x);
y = find(y);
while (d[x] > d[lc]){
ans--;
// root[x] = root[f[x][0]];
root[x] = find(f[x][0]);
x = f[x][0];
x = find(x);
}
while (d[y] > d[lc]){
ans--;
// root[y] = root[f[y][0]];
root[y] = find(f[y][0]);
y = f[y][0];
y = find(y);
}
}
printf("%d\n", ans);
}
puts("");
}
return 0;
}