POJ 3694 network tarjan求桥 + LCA + e_DCC缩点

题目链接(You can click it.)

题目含义:

有一张无向图,在其中添边。问每次添边之后,还剩多少桥。

一种较为简单的解法(不涉及e_DCC缩点)(You can click it.)

接下来介绍一个设计e_DCC缩点的思路。e_DCC缩点其实就是将一个边双连通分量看作是一个点,所以需要求出所有的边双连通分量。缩点的操作如下:

  1. 求桥(tarjan)
  2. 求不含桥边的连通块(每一个连通块就是一个e_DCC边双连通分量了)
  3. 将每一个连通块当作一个点处理

然后当然还要重新建图了(要不然缩点干嘛),重新建图的时候就遍历所有的边,如果每个边的两个端点不在同一个边双连通分量,那么他们的连边就是割边,e_DCC缩点的新图中,边都是原图中的割边,所以直接将两个点所在的边双连通分量连边即可。

然后对新图,求每个点(边双连通分量)的深度(求LCA要用),初始状态的桥的数量为缩点后的点数量-1(有n个点显然就是n-1个边了,因为缩点后是不会有环的,而且新图的边全部都是桥),对于每次操作,如果两个点在同一个边双连通分量,显然操作是无效的如果不在同一个边双连通分量,就求一下两个点对应的新点(就是两个点分别在哪个边双连通分量中),求两个新点的LCA,并且在想上返回的图中,把遇到的桥边标记为非桥边因为操作后它显然已经不是桥边了),然后就是每遇到这么一个边, a n s = a n s − 1 ans = ans - 1 ans=ans1即可。

也许你也注意到了,我们还要将每个新点的父节点求出来(方便向上返回,可以在求深度的时候顺便求出),还需要求出桥边(因为是新图,其实每个边都是桥边,完全可以不考虑这个,想了想还是提一下吧),用一个数组 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 的所有的边都遍历一下(向上扫描),因此求无论如何都需要扫描一遍,就不采用倍增的方式了。

但是还可以优化,写在另一篇博客里面吧。

猜你喜欢

转载自blog.csdn.net/weixin_43701790/article/details/105335798
今日推荐