无向图的双连通分量

若一张无向图不存在割点,则称它为“点双连通图“;若一张无向图不存在桥,则称它为”边双连通图“。

无向图的极大点双连通子图称为“点双连通分量”,简记为“\(\text{V-DCC(Vertex Double Connected Component)}\)”或“点双”;无向图的极大边双连通子图称为“边双连通分量”,简记为“\(\text{E-DCC(Edge Double Connected Component)}\)”或”边双“。二者合称”双连通分量“,简记为”\(\text{DCC(Double Connected Component)}\)“。

需要注意的是,一个孤立点也算是 \(\text{V-DCC}\)\(\text{E-DCC}\)

V-DCC

如图:

图中 \((5,2,6),(1,3,5),(3,4)\)\(\text{V-DCC}\)

值得注意的是,一个割点可能属于多个 \(\text{V-DCC}\)

我们维护一个栈,在 \(\operatorname{dfs}\) 的时候执行以下过程:

  1. 将当前节点 \(u\) 入栈;
  2. \(dfn(u)\le low(v)\) 时,我们不需要判断 \(u\) 是否为 \(root\),直接一直弹栈,直到弹出 \(v\),相当于弹出了 \(subtree(v)\),则 \(subtree(v)+u\) 构成一个 \(\text{V-DCC}\)

T103492 【模板】点双连通分量

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

const int MAXN = 5e4 + 5;
const int MAXM = 3e5 + 5;

int cnt, Time, rt, tot;
int head[MAXN], dfn[MAXN], low[MAXN];
stack<int> sta;
vector<int> dcc[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;
	if (u == rt && !head[u]) //特判孤立点
	{
		dcc[++tot].push_back(u);
		return;
	}
	sta.push(u);
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if (dfn[u] <= low[v])
			{
				tot++;
				int now = 0;
				while (v != now) //弹出 subtree(v)
				{
					now = sta.top();
					sta.pop();
					dcc[tot].push_back(now);
				}
				dcc[tot].push_back(u);
			}
		}
		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);
		}
	}
	for (int i = 1; i <= tot; i++)
	{
		for (int j = 0; j < dcc[i].size(); j++)
		{
			printf("%d ", dcc[i][j]);
		}
		putchar('\n');
	}
	return 0;
}

E-DCC

显然,桥不可能存在于 \(\text{E-DCC}\) 中,所以我们直接把图中的所有桥都去掉,剩下的连通块就是所有的 \(\text{E-DCC}\) 了。

下图中共 \(3\) 条桥,\(4\)\(\text{E-DCC}\)


\(\rm Tarjan\) 算出所有的桥,再 \(\rm dfs\) 一遍,不访问桥边,这样就能求出每个连通块了。

T103489 【模板】边双连通分量

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

const int MAXN = 5e4 + 5;
const int MAXM = 3e5 + 5;

int cnt = 1, Time, dcc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool bridge[MAXM << 1];

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, 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])
			{
				bridge[i] = bridge[i ^ 1] = true;
			}
		}
		else if (i != (in_edge ^ 1))
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
}

void dfs(int u)
{
	c[u] = dcc;
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (c[v] || bridge[i]) //忽略一个访问过的和桥边
		{
			continue;
		}
		dfs(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);
		}
	}
	for (int i = 1; i <= n; i++)
	{
		if (!c[i]) //一个新的连通块
		{
			dcc++;
			dfs(i);
		}
	}
	printf("%d\n", dcc);
	return 0;
}

Problem A:连通分量

题意

给一个无向图,询问两点是否属于同一个 \(\text{V/E-DCC}\)(数据保证一个点属于不超过 \(10\) 个点双连通分量) 。

几乎就是模板,需要注意的是由于割点会属于多个 \(\text{V-DCC}\),所以点双的 \(c\) 数组得开成 \(\rm vector\),判断的时候双重循环,由于数据有保证,所以双重循环最多进行 \(10^2=100\) 次,时间复杂度 \(\operatorname{O}(n+100q)\)

\(\text{Code}\)

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

const int MAXN = 3e5 + 5;
const int MAXM = 5e5 + 5;

struct v_dcc
{
	int cnt, Time, rt, dcc;
	int head[MAXN], dfn[MAXN], low[MAXN];
	stack<int> sta;
	vector<int> c[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;
		sta.push(u);;
		if (u == rt && !head[u])
		{
			c[u].push_back(++dcc);
			return;
		}
		for (int i = head[u]; i; i = e[i].nxt)
		{
			int v = e[i].to;
			if (!dfn[v])
			{
				tarjan(v);
				low[u] = min(low[u], low[v]);
				if (dfn[u] <= low[v])
				{
					dcc++;
					int now = 0;
					while (v != now)
					{
						now = sta.top();
						sta.pop();
						c[now].push_back(dcc);
					}
					c[u].push_back(dcc);
				}
			}
			else
			{
				low[u] = min(low[u], dfn[v]);
			}
		}
	}
}cut;

struct e_dcc
{
	int cnt = 1, Time, dcc;
	int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
	bool bridge[MAXM << 1];
	
	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, 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])
				{
					bridge[i] = bridge[i ^ 1] = true;
				}
			}
			else if (i != (in_edge ^ 1))
			{
				low[u] = min(low[u], dfn[v]);
			}
		}
	}
	
	void dfs(int u)
	{
		c[u] = dcc;
		for (int i = head[u]; i; i = e[i].nxt)
		{
			int v = e[i].to;
			if (c[v] || bridge[i])
			{
				continue;
			}
			dfs(v);
		}
	}
}bridge;

int main()
{
	int n, m, q;
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1; i <= m; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		cut.add(u, v);
		cut.add(v, u);
		bridge.add(u, v);
		bridge.add(v, u);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!cut.dfn[i])
		{
			cut.tarjan(cut.rt = i);
		}
	}
	for (int i = 1; i <= n; i++)
	{
		if (!bridge.dfn[i])
		{
			bridge.tarjan(i, 0);
		}
	}
	for (int i = 1; i <= n; i++)
	{
		if (!bridge.c[i])
		{
			bridge.dcc++;
			bridge.dfs(i);
		}
	}
	while (q--)
	{
		int op, x, y;
		scanf("%d%d%d", &op, &x, &y);
		if (op == 1)
		{
			bool flag = false;
			for (int i = 0; i < cut.c[x].size(); i++)
			{
				for (int j = 0; j < cut.c[y].size(); j++)
				{
					if (cut.c[x][i] == cut.c[y][j] && !flag)
					{
						puts("yes");
						flag = true;
					}
				}
			}
			if (!flag)
			{
				puts("no");
			}
		}
		else
		{
			if (bridge.c[x] == bridge.c[y])
			{
				puts("yes");
			}
			else
			{
				puts("no");
			}
		}
	}
	
	return 0;
}

Problem B:多余的路径

P2860 [USACO06JAN]Redundant Paths G

题意

给定一张无向连通图,求最少往图中加入几条边,使得原图变为一个 \(\text{E-DCC}\)

思路

可以发现同一个 \(\text{E-DCC}\) 内的节点都已经满足要求,所以我们进行缩点,同时统计度数。

缩点后,入度为 \(1\) 的节点一定是叶子节点,我们在两个叶子节点间连一条边即可。

设有 \(k\) 个入度为 \(1\) 的节点,若 \(k\) 是偶数,答案为 \(\frac{k}{2}\);若 \(k\)​ 为奇数,则我们在 \((k-1)\) 个节点间连边后还有一个节点,它随便连一条边即可。

综上,答案为 \(\left\lceil\frac{k}{2}\right\rceil\)。​

Code

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

const int MAXN = 5e3 + 5;
const int MAXM = 1e4 + 5;

int cnt = 1, Time, dcc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN], du[MAXN];
bool bridge[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, 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])
	 		{
	 			bridge[i ^ 1] = bridge[i] = true;
			}
		}
		else if (i != (in_edge ^ 1))
		{
			low[u] = min(low[u], dfn[v]);
		}
	 }
}

void dfs(int u)
{
	c[u] = dcc;
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (c[v] || bridge[i])
		{
			continue;
		}
		dfs(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);
		}
	}
	for (int i = 1; i <= n; i++)
	{
		if (!c[i])
		{
			dcc++;
			dfs(i);
		}
	}
	for (int i = 2; i <= cnt; i += 2)
	{
		int u = e[i ^ 1].to, v = e[i].to;
		if (c[u] != c[v])
		{
			du[c[u]]++;
			du[c[v]]++;
		}
	}
	int ans = 0;
	for (int i = 1; i <= dcc; i++)
	{
		if (du[i] == 1)
		{
			ans++;
		}
	}
	printf("%d\n", (ans + 1) >> 1);
	return 0;
}
posted @ 2021-08-09 11:52  mango09  阅读(192)  评论(0编辑  收藏  举报
-->