无向图的双连通分量

定义

在一张连通的无向图中,对于两个点u和v,如果无论删去哪条边(只能删去一条)都不能使得它们不连通,则称u和v边双连通

在一张连通的无向图中,对于两个点u和v,如果无论删去哪个点(只能删去一个,且不能删去u和v自己)都不能使得它们不连通,则称u和v点双连通

边双连通具有传递性,即若x,y边双连通,y,z边双连通,则x,z边双连通。

点双连通不具有传递性。

割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。

割边:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

注意:割点其实和割边并没有直接关系


例题

冗余路径

在这里插入图片描述

本题即是问:最少加几条边可以使得该无向图变成一个边双连通分量

缩点后该图就会成为一棵树,答案就是树中度为1的节点除2上取整。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 5010, M = 20010;

int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int stk[N], top;
bool bridge[N];
int dcc_cnt, id[N], degree[N];

void add(int u, int v)
{
	e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}

void tarjan(int u, int from) //from:从哪条边过来
{
	dfn[u] = low[u] = ++ timestamp;
	stk[++ top] = u;

	for(int i = head[u]; ~i; i = ne[i])
	{
		int v = e[i];
		if(!dfn[v])
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);
			if(dfn[u] < low[v])	bridge[i] = bridge[i ^ 1] = true;
			//说明 v 无论如何也走不到 u 已经 u 以上的点,u--> v这条边就是一个桥
		}
		else if(i != (from ^ 1)) //不是反向边
			low[u] = min(low[u], dfn[v]);
	}

	if(dfn[u] == low[u])
	{
		++ dcc_cnt;
		int p;
		do {
			p = stk[top --];
			id[p] = dcc_cnt;
		} while(p != u);
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	cin >> n >> m;
	memset(head, -1, sizeof head);
	while(m --)
	{
		int u, v;
		cin >> u >> v;
		add(u, v), add(v, u);
	}

	tarjan(1, -1);
	
	for(int i = 0; i < idx; i ++)
		if(bridge[i])	degree[id[e[i]]] ++;
	
	int cnt = 0;
	for(int i = 1; i <= dcc_cnt; i ++)	cnt += (degree[i] == 1);

	cout << (cnt + 1) / 2 << endl;

	return 0;
}

电力

在这里插入图片描述

如果删除一个点以后联通块个数会增加,意味着该点一定是图中的一个割点,否则删除不是割点的点的话联通块个数并不会改变。

所以本题做法很明显,求出所有割点,枚举删除哪一个割点会得到最大值。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 10010, M = 30010;

int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int root; //记录每一个连通块的“根节点”
int ans; //记录答案最大值

void add(int u, int v)
{
	e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++ timestamp;

	int cnt = 0; //如果u是割点的话,去掉该点u得到的连通块个数。(也相当于缩点后u的儿子个数)
	for(int i = head[u]; ~i; i = ne[i])
	{
		int v = e[i];
		if(!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u])	cnt ++;
			//v无论如何都到不了u的上面,那么去掉u之后,v所在连通块会与u上面的连通块分开,即u是割点
		}
		else	low[u] = min(low[u], dfn[v]);
	}

	if(u != root)	cnt ++; //u不是根,那么去掉u之后,u上面也会分离出来一个连通块

	ans = max(ans, cnt);
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	while(scanf("%d%d", &n, &m), n || m)
	{
		memset(dfn, 0, sizeof dfn);
		memset(head, -1, sizeof head);
		idx = timestamp = 0;

		while(m --)
		{
			int u, v;
			scanf("%d%d", &u, &v);
			add(u, v), add(v, u);
		}

		ans = 0;
		int cnt = 0; //原图连通块个数
		for(root = 0; root < n; root ++)
			if(!dfn[root])
			{
				cnt ++;
				tarjan(root);
			}
		
		printf("%d\n", ans + cnt - 1);
	}

	return 0;
}

矿场搭建

在这里插入图片描述

显然:出口个数一定要大于等于2。

分情况看:

1、整个图没有割点

​ 则不管删除哪一个点,整个图依然是连通的,所以只要有2个出口就可以,并且这2个出口的位置可以随便放,方案数是 C c n t 2 C^2_{cnt} Ccnt2 。(cnt是点的个数)

2、有割点

​ 缩点后,整个图就变成了一棵树。

​ 具体缩点方法是:

​ 1)每个割点单独作为一个点

​ 2)从每个V-DCC(点双连通分量)向其所包含的每个割点连一条边

​ 如果V-DCC的度数为1,意味着它只有一个割点,如果该割点坏掉,该点双连通分量与其他的点就分开了,所以该点连通分量内部(非割点位置)必须要设置一个出口,方案数为cnt-1。(cnt是该点双连通分量的大小,-1是除去了割点)

​ 如果V-DCC读数大于1,即它包含大于1个割点。那么不管哪一个点坏掉,该点连通分量总数可以连接到其他的点,所以这样的点双连通分量内部不需要设置出口。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef unsigned long long ULL;

const int N = 1010, M = 1010;

int n, m;
int idx, head[N], e[M], ne[M];
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt, root;//双连通分量个数
vector<int> dcc[N];//双连通分量内包含哪些点
bool cut[N];//是否是割点

void add(int u, int v)
{
	e[idx] = v, ne[idx] = head[u], head[u] = idx ++;
}

void tarjan(int u)
{
	dfn[u] = low[u] = ++ timestamp;
	stk[++ top] = u;
	//1、u是孤立点--自成一个v-dcc
	if(u == root && head[u] == -1) //u是根且没有邻边
	{
		dcc_cnt ++;
		dcc[dcc_cnt].push_back(u);
		return ;
	}
	//2、u不是孤立点
	int cnt = 0; //若u是割点,cnt就是其儿子节点的个数
	for(int i = head[u]; ~i; i = ne[i])
	{
		int v = e[i];
		if(!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u]) //从v遍历不到u的上面,u可能是割点
			{
				cnt ++;
				if(u != root || cnt > 1)	cut[u] = true;
				//如果u不是根或者u是根但是u的儿子个数大于1,那么u就是割点
				++ dcc_cnt;
				int y;
				do {
					y = stk[top --];
					dcc[dcc_cnt].push_back(y);
				} while(y != v);//注意:结束条件是y != v,u仍然保留在栈中
				//因为u被同时包含在两个点双连通分量中,它还要用于更高的一个连通块
				//如果这个时候吧u出栈,在回溯到比u更高的点的时候,u就无法被加入该块内
				dcc[dcc_cnt].push_back(u); //最后把u放入
			}
		}
		else	low[u] = min(low[u], dfn[v]);
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	int kase = 1;
	while(cin >> m, m)
	{
		memset(head, -1, sizeof head);
		memset(dfn, 0, sizeof dfn);
		memset(cut, 0, sizeof cut);
		for(int i = 1; i <= dcc_cnt; i ++)	dcc[i].clear();
		idx = timestamp = dcc_cnt = top = n = 0;

		while(m --)
		{
			int u, v;
			cin >> u >> v;
			add(u, v), add(v, u);
			n = max(n, max(u, v));
		}

		for(root = 1; root <= n; root ++)
			if(!dfn[root])	tarjan(root);
	
	    int ans = 0; //最少出口数量
    	ULL nums = 1;//方案数
    	for(int i = 1; i <= dcc_cnt; i ++) //枚举每一个点双连通分量
    	{
    		int cnt = 0; //该点双连通分量包含的割点的个数
    		for(auto x: dcc[i])	cnt += cut[x];
    		
    		if(!cnt)	
			{ 
				if(dcc[i].size() > 1)	ans += 2, nums *= dcc[i].size() * (dcc[i].size() - 1) / 2;
				else	ans ++;//特殊判断,孤立点
			}
    		else if(cnt == 1)	ans ++, nums *= dcc[i].size() - 1;
    	}
    
    	printf("Case %d: %d %llu\n", kase ++, ans, nums);
	}

	return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值