Dyd's Blog

He who has a strong enough why can bear almost any how.

圆方树

又一个奇怪的树

圆方树

圆方树(Block forest)是一种树的结构,它能轻松的将仙人掌转化为树,并支持一些操作,有时我们也可以在一般无向图上使用它

定义

先明确一些基本定义:

  1. 仙人掌:每条边在不超过一个简单环中的无向图
  2. 点双连通分量:图中任意两不同点之间都有至少两条点不重复的路径

要构造圆方树,当然是要构造出圆点方点,圆点比较简单,直接保留仙人掌上的点即可;然后对仙人掌进行点双 $Tarjan$ 缩点,每一个点双就是一个方点

对于边,原图上的边统一不要,直接从所以方点向它对应的点双包括的圆点连边,这样,每一个点双就形成了以方点为根,圆点为叶的“菊花图”,而这很多个“菊花图”通过原图中的割点连接在一起(因为点双的分隔点是割点),如图:

圆方树

圆方树的点数小于 $2n$ ,所以请注意各种数组大小要开两倍

其实,如果原图连通,则“圆方树”才是一棵树,如果原图有 $k$ 个连通分量,则它的圆方树也会形成 $k$ 棵树形成的森林

如果原图中某个连通分量只有一个点,则需要具体情况具体分析,我们在后续讨论中不考虑孤立点

构造

直接对原图进行 $Tarjan$ ,因为原图上每个环都是一个点双,而且在栈中的顺序就是环上点的顺序,如果一个点 $i$ 的出边 $(i, j)$ 满足 $dfn(i) < low(j)$ ,说明 $(i, j)$ 是一条树边,直接加上即可;如果 $dfn(i) = low(j)$ ,那么我们找到了一个环(可能是重边造成的二元环),则从栈中取出点直到取出 $j$ 为止,设这样从栈中取出的点集为 $R$ ,则 $i$ 和 $R$ 构成一个环

对于环,我们新建一个方点,对环上每个点连边即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <bits/stdc++.h>
#define STC static
using namespace std;
const int N = 1e5 + 5;
vector<int> G[N], T[N * 2];
int n, m, cnt;
int stk[N], top;
int dfn[N], low[N], dfc = 0;
void tarjan(int x)
{
low[x] = dfn[x] = ++dfc;
stk[++top] = x;
for (auto y : G[x])
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y] == dfn[x])//标志着找到一个以x为根的点双连通分量
{
++cnt; //增加方点个数
for (int z = 0; z != y; --top) //这里一定要让z=0!
{
z = stk[top];
T[cnt].push_back(z);
T[z].push_back(cnt);
}
//注意x自身也要连边(但不退栈)
T[cnt].push_back(x);
T[x].push_back(cnt);
}
}
else
low[x] = min(low[x], dfn[y]);
}
int main()
{
scanf("%d %d", &n, &m);
cnt = n;
for (int i = 1, u, v; i <= m; ++i)
{
scanf("%d %d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
tarjan(i), --top;
//注意到退出Tarjan时栈中还有一个元素即根,将其退栈
cout << endl << endl;
for (int i = 1; i <= cnt; ++i)
{
for (auto j : T[i])
{
cout << i << " " << j << endl;
}
}
return 0;
}

性质

圆方树有优美的性质:

  1. 无论如何换根,圆方树形态不变,因为你是把环连成菊花而不是别的什么
  2. 圆方树的子树 = 原仙人掌的子仙人掌,分类讨论各种情况可以证明
  3. 方点不会和方点相连

还有其他神奇的性质,要具体题目具体分析

一些应用

下面开始口胡

仙人掌最短路

类比树上最短路,我们在LCA处考虑,然而圆方树的树上距离并不一定就等于仙人掌上的最短路长度

注意到圆方树中的边权都还没有确定,可以利用,如果一条边是圆圆边(圆点指向圆点的边,这里已经任意选择一圆点为根),那么它是一个树边,边权等于原仙人掌上边权;如果是方圆边,那么边权等于环的根到那个圆点的最短路径长度;如果是圆方边,那么边权为 $0$

这样,如果一对询问点在圆方树上的LCA是圆点,那么其圆方树上长度就是原仙人掌上长度,因为路径上唯一的不同就在于对于每个环是走哪一侧(这个决策对每个环是独立的),而前面边权的确定已经解决了这一问题;

如果LCA是方点,则我们只需要考虑在LCA这个环中是否要走经过根的那一侧,需要找出这两个询问点 $(x, y)$ 分别是在这个方点的哪两个儿子 $(px, py)$ 的子树中,求同一个环上的点 的最短路径(其实也就是走哪一侧的问题 )可以先记录环上边权的前缀和,这样就能 $O(1)$ 计算出不经过根那条路径的长度,再通过记录整个环的边权和比较出走哪一侧更优

仙人掌dp

仙人掌有若干种DP,比如最大独立集和直径

最大独立集比较简单,只需要DFS时,树边按照树上独立集的方式转移,环上用一个 f[i][0/1][0/1] 的简单DP去转移即可

求直径的话,我们类比求树直径的方式,在LCA处考虑,设 f[i][1/2] 为 $i$ 子树中的第 $j$ 深的点,那么圆点可以轻松转移、更新答案;在方点转移也容易,但更新答案不能直接更新(因为有哪一侧的问题,直接取不一定是最短路,会导致答案偏大),我们可以把环复制一份用单调队列来解决,也可以正反各做一次前缀和

等等

算了,反正我也不会,看这个

例题

还是给道题:

战略游戏

这是一道图上圆方树

先建出圆方树,则要求删掉多少个圆点后 $S$ 在圆方树上对应的圆点不全连通,这就是连通子图中的圆点个数减去 $|S|$ ,或者说,圆方树上 $S$ 中节点两两间路径(树上只有唯一路径)的点集并减去 $S$

如何计算连通子图中的圆点个数?有一个方法:

把圆点的权值放到它和它的父亲方点的边上,问题转化为求边权和,即把 $S$ 中的点按照DFS序排序,计算排序后相邻两点的距离和(还包括首尾两点之间的距离),答案就是距离和的一半,因为每条边只被经过两次

最后,如果子图中的深度最浅的节点是圆点,答案还要加上 $1$ ,因为我们没有统计到它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <bits/stdc++.h>
#define STC static
using namespace std;
const int N = 1e5 + 5, D = 25;
vector<int> G[N], T[N << 1];
int n, m, cnt, q;
int stk[N], top;
int dfn[N << 1], low[N], dfc = 0;
int dep[N << 1], f[N << 1][D + 5], dis[N << 1];
void tarjan(int x)
{
low[x] = dfn[x] = ++dfc;
stk[++top] = x;
for (auto y : G[x])
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y] == dfn[x])
{
++cnt;
for (int z = 0; z != y; --top)
{
z = stk[top];
T[cnt].push_back(z);
T[z].push_back(cnt);
}
T[cnt].push_back(x);
T[x].push_back(cnt);
}
}
else
low[x] = min(low[x], dfn[y]);
}
bool cmp(int x, int y)
{
return dfn[x] < dfn[y];
}
void dfs(int x, int fa)
{
dfn[x] = ++dfc;
dep[x] = dep[f[x][0] = fa] + 1;
dis[x] = dis[fa] + (x <= n);
for (int i = 1; i <= D; ++i)
f[x][i] = f[f[x][i - 1]][i - 1];
for (auto y : T[x])
if (y != fa)
dfs(y, x);
}
int lca(int x, int y)
{
if (dep[x] > dep[y])
swap(x, y);
for (int i = D; i >= 0; --i)
if (dep[f[y][i]] >= dep[x])
y = f[y][i];
if (y == x)
return x;
for (int i = D; i >= 0; --i)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
}
int main()
{
int TT;
scanf("%d", &TT);
while (TT--)
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; ++i)
{
G[i].clear();
dfn[i] = low[i] = 0;
}
for (int i = 1; i <= (n << 1); ++i)
T[i].clear();
for (int i = 1, u, v; i <= m; ++i)
{
scanf("%d %d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
cnt = n;
dfc = 0, tarjan(1), --top;
dfc = 0, dfs(1, 0);
scanf("%d", &q);
while (q--)
{
STC int s, a[N];
scanf("%d", &s);
int ans = s * -2;
for (int i = 1; i <= s; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + s, cmp);
for (int i = 1; i <= s; ++i)
{
int u = a[i], v = a[i % s + 1];
ans += dis[u] + dis[v] - 2 * dis[lca(u, v)];
}
if (lca(a[1], a[s]) <= n)
ans += 2;
printf("%d\n", ans / 2);
}
}
return 0;
}