无向图的割点与桥

给定无向连通图:

  • 对于其中一点 u u u,若从图中删掉 u u u 和所有与 u u u 相连的边后,原图分裂成成 2 2 2 个或以上不相连的子图,则称 u u u 为原图的割点(或割顶)
  • 对于其中一边 e e e,若从图中删掉 e e e 后,原图分裂成 2 2 2 个或以上不相连的子图,则称 e e e 为原图的桥(或割边)
  • 一般无向图(不保证连通)的割点与桥就是它各个连通块的割点与桥。

T a r j a n \rm Tarjan Tarjan 算法可以在 O ⁡ ( n ) \operatorname{O}(n) O(n) 内求出所有割点与桥。

跟求 S C C \rm SCC SCC 类似,我们也需要用到 d f n dfn dfn l o w low low 数组,其意义和求 S C C \rm SCC SCC 时的 d f n , l o w dfn,low dfn,low 数组类似。

1. 割点

u u u 不是搜索树的 r o o t root root,则 u u u 是割点当且仅当树上至少有 u u u 1 1 1 个子节点 v v v 满足:
d f n ( u ) ≤ l o w ( v ) dfn(u)\le low(v) dfn(u)low(v)
u u u r o o t root root,则 u u u 是割点当且仅当 u u u 至少有 2 2 2 个子节点满足上述条件。

d f n ( u ) ≤ l o w ( v ) dfn(u)\le low(v) dfn(u)low(v) 说明从 s u b t r e e ( v ) subtree(v) subtree(v) 出发,若不经过 u u u,则无法到达比 u u u d f n dfn dfn 更小的节点,那么我们把 u u u 删掉,原图就被分成了 s u b t r e e ( v ) subtree(v) subtree(v) 和剩下的节点至少 2 2 2 个子图。

P3388 【模板】割点(割顶)

#include <iostream>
#include <cstdio>
using namespace std;

const int MAXN = 2e4 + 5;
const int MAXM = 1e5 + 5;

int cnt, Time, rt, tot;
int head[MAXN], dfn[MAXN], low[MAXN];
bool cut[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; //初始化dfn和low
	int flag = 0;
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (!dfn[v]) //low值的更新和求SCC时类似
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if (dfn[u] <= low[v])
			{
				flag++;
				if (u != rt || flag > 1) //满足x不是根节点,或者x是根节点且有至少2个满足要求的子节点
				{
					if (!cut[u]) //防止重复统计
					{
						tot++;
					}
					cut[u] = true;
				}
			}
		}
		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); //搜索树的根是i
		}
	}
	printf("%d\n", tot);
	for (int i = 1; i <= n; i++)
	{
		if (cut[i])
		{
			printf("%d ", i);
		}
	}
	return 0;
}

2. 桥

搜索树上 u u u 的子节点是 v v v,则边 < u , v > <u,v> <u,v> 是桥,当且仅当:
d f n ( u ) < l o w ( v ) dfn(u)<low(v) dfn(u)<low(v)

d f n ( u ) < l o w ( v ) dfn(u)<low(v) dfn(u)<low(v) 说明从 s u b t r e e ( v ) subtree(v) subtree(v) 出发,若不经过 < u , v > <u,v> <u,v>,则无法到达 比 u u u d f n dfn dfn 更小的节点,那么我们把 < u , v > <u,v> <u,v> 删掉,原图就被分成了 s u b t r e e ( v ) subtree(v) subtree(v) 和剩下的节点至少 2 2 2 个子图。

值得注意的是:因为是无向边,所以从 u u u 出发总能回到它的 f a fa fa。根据 l o w low low 的定义, < u , f a > <u,fa> <u,fa> 是树边且 f a ∉ s u b t r e e ( u ) fa\notin subtree(u) fa/subtree(u),所以 d f n ( f a ) dfn(fa) dfn(fa) 不能用来更新 l o w ( u ) low(u) low(u)!!!

但是你以为这样就完了吗?

毒瘤数据会出现重边!!!

对于重边,只有一条算树边,所以有重边时, d f n ( f a ) dfn(fa) dfn(fa) 又能用来更新 l o w ( u ) low(u) low(u) 了。

机房某 dalao:你 ∗ * 炸了

处理方法:将读入的边成对储存在 e ( 2 ) e(2) e(2)​ 和 e ( 3 ) e(3) e(3)​, e ( 4 ) e(4) e(4)​ 和
e ( 5 ) … e ( 2 n ) e(5)\dots e(2n) e(5)e(2n)​ 和 e ( 2 n + 1 ) e(2n+1) e(2n+1)​ 里。

观察:

2   xor ⁡   1 = 3 2\,\operatorname{xor}\,1=3 2xor1=3​​

4   xor ⁡   1 = 5 4\,\operatorname{xor}\,1=5 4xor1=5​​

⋯ ⋯ \cdots\cdots

2 n   xor ⁡   1 = 2 n + 1 2n\,\operatorname{xor}\,1=2n+1 2nxor1=2n+1​​

若通过 e ( i ) e(i) e(i)​ 进入 u u u​,则 e ( i ) e(i) e(i)​ 和 e ( i xor ⁡ 1 ) e(i\operatorname{xor}1) e(ixor1)​ 本质上是同一条无向边,故除了 e ( i xor ⁡ 1 ) e(i\operatorname{xor}1) e(ixor1)​ 之外的边都能用来更新 l o w ( u ) low(u) low(u)​。

P1656 炸铁路

另外吐槽一句,这题作为桥的模板本来应该评绿的,结果因为数据范围较小可以用暴力过直接给评黄了……

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int MAXN = 155;
const int MAXM = 5005;

int cnt = 1, Time, tot; //注意这里!因为边是存在(2,3),(4,5)……内的,所以cnt要初始化为1!
int head[MAXN], dfn[MAXN], low[MAXN];

struct edge
{
	int to, nxt;
}e[MAXM << 1];

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

struct ans
{
	int from, to;
	bool operator <(const ans &x)const
	{
		if (x.from != from)
		{
			return x.from > from;
		}
		return x.to > to;
	}
}a[MAXM << 1];

void add_ans(int u, int v)
{
	a[++tot] = ans{min(u, v), max(u, v)};
}

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]) //是桥,把答案存进去
			{
				add_ans(u, v);
			}
		}
		else if (i != (in_edge ^ 1)) //不是同一条无向边
		{
			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(i, 0);
		}
	}
	sort(a + 1, a + tot + 1); //按照题目要求输出
	for (int i = 1; i <= tot; i++)
	{
		printf("%d %d\n", a[i].from, a[i].to);
	}
	return 0;
}

一个好玩的性质

除两点一线的情况外,桥的两个端点一定都是割点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值