圆方树
前置知识
点双连通分量
以下简称点双连通分量为点双。
定义
设 $G = (V, E)$ 是一个连通无向图,$K$ 是 $G$ 的点双,如果 $K$ 中任意两点 $u, v$ 都有路径相连,则称 $K$ 是 $G$ 的点双。
性质
- 两个点双最多有一个公共点,且这个点为割点。
- 对于一个点双,它在
DFS
搜索树中dfn
值最小的点一定是割点或者树根。
求解算法
可以对于第 $2$ 个性质分类讨论。
- 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
- 当这个点为树根时:
有两个及以上子树,它是一个割点。只有一个子树,它是一个点双连通分量的根。它没有子树,视作一个点双。
代码见oi-wiki。
现在切入正题。
圆方树的定义
对于图中的任意一个点双,将它的每个点连到一个我们新建出来的点上,并删除原来的所有边,这样得出的图称为圆方树。
定义原来图上就有的点叫做圆点,被新建立出来的点叫做方点。
圆方树的性质
-
如果原图连通,那么它一定是一颗树。(所以圆方树这个名字非常形象)
-
如果一个圆点连接着两及以上个方点,那么它一定是一个割点。
圆方树的构造
下图给出了圆方树的构造过程:
来源 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
一边,把