圆方树

圆方树

前置知识

点双连通分量

以下简称点双连通分量为点双。

定义

\(G = (V, E)\) 是一个连通无向图,\(K\)\(G\) 的点双,如果 \(K\) 中任意两点 \(u, v\) 都有路径相连,则称 \(K\)\(G\) 的点双。

性质

  1. 两个点双最多有一个公共点,且这个点为割点。
  2. 对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。

求解算法

可以对于第 \(2\) 个性质分类讨论。

  1. 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
  2. 当这个点为树根时:
    有两个及以上子树,它是一个割点。只有一个子树,它是一个点双连通分量的根。它没有子树,视作一个点双。

代码见oi-wiki


现在切入正题。

圆方树的定义

对于图中的任意一个点双,将它的每个点连到一个我们新建出来的点上,并删除原来的所有边,这样得出的图称为圆方树。

定义原来图上就有的点叫做圆点,被新建立出来的点叫做方点。

圆方树的性质

  1. 如果原图连通,那么它一定是一颗树。(所以圆方树这个名字非常形象)

  2. 如果一个圆点连接着两及以上个方点,那么它一定是一个割点。

圆方树的构造

下图给出了圆方树的构造过程:

圆方树的构造过程(原图)
圆方树的构造过程(建点)
圆方树的构造过程(连边和删边)

来源 oi-wiki

算法求解

直接利用 Tarjan,在求解边双的同时,进行连边和删边操作即可。

代码实现

实现
void Tarjan(int u) {
	dfn[u] = low[u] = ++ sign;
	st.push(u);
	for (auto v : g[u]) {
		if (!dfn[v]) {
			Tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] == dfn[u]) {
				tot ++ ;
				int y;
				do {
					y = st.top();
					st.pop();
					add(y, tot, New), add(tot, y, New);
				} while (y != v);
				add(u, tot, New), add(tot, u, New);
			}
		} else low[u] = min(low[u], dfn[v]);
	}
}

经典例题

AT_abc318_g

没有多测,直接不可以总司令。

一眼圆方树板题,先构建圆方树,再看 \(a\)\(c\) 的路径上有没有一个方点能够到达 \(b\)

如果不理解,画个图就明白了。

实现
#include <bits/stdc++.h>
using namespace std;

const int N = 4e5 + 10;

int n, m;
int a, b, c;
vector<int> g[N], New[N];
int tot;
int low[N], dfn[N], sign;
stack<int> st;

void add(int a, int b, vector<int> g[]) {
	g[a].push_back(b);
}

void Tarjan(int u) {
	dfn[u] = low[u] = ++ sign;
	st.push(u);
	for (auto v : g[u]) {
		if (!dfn[v]) {
			Tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] == dfn[u]) {
				tot ++ ;
				int y;
				do {
					y = st.top();
					st.pop();
					add(y, tot, New), add(tot, y, New);
				} while (y != v);
				add(u, tot, New), add(tot, u, New);
			}
		} else low[u] = min(low[u], dfn[v]);
	}
}

int pre[N];
bool flag;

void Get_path(int u) {
	if (u == c) {
		flag = true;
		return ;
	}
	for (auto v : New[u]) {
		if (v == pre[u]) continue;
		if (!flag) {
			pre[v] = u;
			Get_path(v);
			if (flag) return ;
		}
	}
}

signed main() {
	cin >> n >> m >> a >> b >> c;
	tot = n;
	while (m -- ) {
		int a, b;
		cin >> a >> b;
		add(a, b, g), add(b, a, g);
	}
	for (int i = 1; i <= n; i ++ )
		if (!dfn[i]) {
			Tarjan(i);
			while (!st.empty()) st.pop();
		}
	Get_path(a);
	int t = c;
	while (t != a) {
		if (t > n) {
			for (auto v : New[t])
				if (v == b) {
					cout << "Yes\n";
					return 0;
				}
		}
		t = pre[t];
	}
	cout << "No\n";
	return 0;
}

P4320 道路相遇

比上面的题难一点,但也不是很难。

显然的,求 \(u\)\(v\) 必经点的数量就是求 \(u\)\(v\) 之间圆点的数量。

但是如果还是像上一题一样,直接求出 \(u\)\(v\) 所经过的点,时间复杂度为 \(O(nq)\),无法通过。

考虑优化,因为是求圆点的数量,不妨给每个点都设一个权值,圆点值为 \(1\),方点值为 \(0\)

所以问题转化成了求 \(u\)\(v\) 之间权值和。

又因为圆方树是一颗树,使用树上路径差分。

定义 \(sum_u\) 为根节点到 \(u\) 的路径上的权值和。

那么答案就是 \(sum_u + sum_v - 2 * sum_{lca(u, v)} + v_{lca(u, v)}\)

其中 \(v_u\) 表示 \(u\) 点的权值。

实现
#include <bits/stdc++.h>
using namespace std;

const int N = 5e5 + 10;

int n, m;
vector<int> g[N], New[N * 2];
int tot;
int low[N], dfn[N], sign;
stack<int> st;
int q;

void add(int a, int b, vector<int> g[]) {
	g[a].push_back(b);
}

void Tarjan(int u) {
	dfn[u] = low[u] = ++ sign;
	st.push(u);
	for (auto v : g[u]) {
		if (!dfn[v]) {
			Tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] == dfn[u]) {
				tot ++ ;
				int y;
				do {
					y = st.top();
					st.pop();
					add(y, tot, New), add(tot, y, New);
				} while (y != v);
				add(u, tot, New), add(tot, u, New);
			}
		} else low[u] = min(low[u], dfn[v]);
	}
}

int pre[N * 2];
bool flag;

int fa[N * 2];
int v[N * 2];
int d[N * 2];

void dfs(int u, int father, int dep) {
    fa[u] = father;
    d[u] = dep;
    if (u <= n) v[u] = 1;
    else v[u] = 0;
    for (auto v : New[u]) {
        if (fa[v]) continue;
        dfs(v, u, dep + 1);
    }
}

int dp[N * 2][40];
int f[N * 2][40];

void ST() {
    memset(dp, -1, sizeof dp);
    for (int i = 1; i <= tot; i ++ ) {
        dp[i][0] = fa[i];
    }
    for (int j = 1; j <= log2(tot); j ++ )
        for (int i = 1; i <= tot; i ++ ) {
            dp[i][j] = dp[dp[i][j - 1]][j - 1];
        }
}

int lca(int x, int y) {
	if (d[x] < d[y]) swap(x, y);
	for (int i = 0, len = d[x] - d[y]; i <= log2(tot); i++, len >>= 1) if (len & 1) x = dp[x][i];
	if (x == y) return x;
	for (int i = log2(n); i >= 0; i--) {
		if (dp[x][i] == dp[y][i]) continue;
		x = dp[x][i];
		y = dp[y][i];
	}
	return dp[x][0];
}

int sum[N * 2];

void dfs2(int u, int father) {
	sum[u] = v[u] + sum[father];
	for (auto v : New[u]) {
		if (v == father) continue;
		dfs2(v, u);
	}
}

signed main() {
	// ios::sync_with_stdio(false);
	// cin.tie(0), cout.tie(0);
	cin >> n >> m;
	tot = n;
	while (m -- ) {
		int a, b;
		cin >> a >> b;
		add(a, b, g), add(b, a, g);
	}
	for (int i = 1; i <= n; i ++ )
		if (!dfn[i]) {
			Tarjan(i);
			while (!st.empty()) st.pop();
		}
    dfs(1, -1, 1);
	// for (int i = 1; i <= tot; i ++ )
	// 	cout << i << ": " << fa[i] << " " << d[i] << " " << v[i] << endl;
    ST();
	dfs2(1, -1);
    cin >> q;
    while (q -- ) {
        int a, b;
        cin >> a >> b;
		int LCA = lca(a, b);
		cout << sum[a] + sum[b] - 2 * sum[LCA] + v[LCA] << endl;
    }
	return 0;
}

P4630 [APIO2018] 铁人两项

也比较简单。

本题考察的是对圆方树的点的赋值。

圆点赋为 \(-1\),方点赋为当前点双的大小。

然后 DFS 一边,把

posted @ 2024-10-24 21:08  zla_2012  阅读(12)  评论(0编辑  收藏  举报