圆方树大概分两种,一个是圆方树,一个是广义圆方树。
圆方树
这可以解决仙人掌上的问题。
任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。
很多题解将其作为无向图构建,本文将其构建为外向树,在这个问题中两种构建方式不会影响求解。
构建方式
记读入的图为原图,构建的圆方树为新图。
首先,新图保留着原图的点集,这些点记为圆点。
将原图任意一个点(实现中指定 $1$ 号点即可)作为根节点,然后在原图跑一遍 dfs。
每当找到一个环的时候(用 tarjan 算法),将进入环的点(也就是边双的根节点)记为头节点,然后在新图上对加一个方点,并让头节点向这个方点连边,同时,方点向其它点 $u$ 连边。
下面使原图与对应新图(圆方树)的一个例子:
tarjan 算法变形
如何才能找到所有的简单环?为了方便,用边双连通分量求解。但是边双是不能轻易处理简单环的,比如这个图:
$1,2,3,4,5,6$ 就是一个边双,但显然不是一个简单环。但是点双可以处理,别的题解模板都用点双。但是本文用边双来处理,两者都是同样的目的。如何改进边双:
原算法中,只要还在栈中,我们都将其看为一个边双联通分量。
但是,现在如果我们还遇到这种情况的话,就应该一个一个的处理为一个一个的简单环,而不是揉在一起。
比如这张图,黑色边组成一颗搜索树,当 $1$ 去搜儿子 $5$ 号点时,发现 $5$ 号点被搜过,那么就可以从 $5$ 号点开始倒推搜索树,一直到 $1$,这样就可以找出简单环。
all in all,看一下代码:
void build_circle(int x, int y, int z) {
for (int i = y; i != x; i = fat[i]) {
...
}
...
}
void tarjan(int u, int from) {
dfn[u] = low[u] = ++idx;
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e, w = ed[p].d;
if (!dfn[v]) {
fat[v] = u, fwt[v] = w, fet[v] = p;
tarjan(v, p);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v]) G[u].push_back({v, w});
}
else if (p != (from ^ 1)) low[u] = min(low[u], dfn[v]);
}
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e, w = ed[p].d;
if (dfn[u] < dfn[v] && fet[v] != p) build_circle(u, v, w); // 开始倒退成环
}
}
圆方树的应用
例题 1:P5236 仙人掌上任意两点间最短路
会建圆方树,但圆方树上边权咋办?每当找到一个环的时候,将进入环的点(也就是边双的根节点)记为头节点,然后在新图上对加一个方点,并让头节点向这个方点连边,边权为 $0$,同时,方点向其它点 $u$ 连边,边权为原图中的 $u$ 到头节点的最短距离。
构建完圆方树后,就不难用其来求两点 $u,v$ 最短路了:
- 当两点 $u,v$ 的最近公共祖先为圆点时,答案就是圆方树上两点的距离,即:$dis[u, v] = d[u] + d[v] - 2d[lca(u, v)]$。($d[u]$ 为 $u$ 到根节点的距离)
- 当两点 $u,v$ 的最近公共祖先为方点时,记 $u,v$ 分别与环交于 $A,B$,注意到 $A,B$ 在环上的距离有两种,我们需要取其中较短的,记为 $k$,那么答案为 $k + dis(u, A) + dis(v, B)$。
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int, int>
#define _for(i, a, b) for (int i = (a); i <= (b); i++)
#define _pfor(i, a, b) for (int i = (a); i >= (b); i--)
#define int long long
const int N = 3e5 + 5;
int n, m, q, dfn[N], low[N], idx, fat[N], fwt[N], fet[N], IDX;
int s[N], stot[N], dep[N], fa[N][20], dist[N], A, B;
int en = 1, first[N];
struct edge {
int e, d, next;
}ed[N];
vector<PII> G[N];
void add_edge(int s, int e, int d) {
en++;
ed[en].e = e, ed[en].d = d;
ed[en].next = first[s];
first[s] = en;
}
void build_circle(int x, int y, int z) {
int sum = z;
for (int i = y; i != x; i = fat[i]) {
s[i] = sum;
sum += fwt[i];
}
s[x] = stot[x] = sum;
G[x].push_back({++IDX, 0});
for (int i = y; i != x; i = fat[i]) {
stot[i] = sum;
G[IDX].push_back({i, min(s[i], sum - s[i])});
}
}
void tarjan(int u, int from) {
dfn[u] = low[u] = ++idx;
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e, w = ed[p].d;
if (!dfn[v]) {
fat[v] = u, fwt[v] = w, fet[v] = p;
tarjan(v, p);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v]) G[u].push_back({v, w});
}
else if (p != (from ^ 1)) low[u] = min(low[u], dfn[v]);
}
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e, w = ed[p].d;
if (dfn[u] < dfn[v] && fet[v] != p) build_circle(u, v, w);
}
}
void dfs(int u, int fat, int depth) {
dep[u] = depth;
for (auto e : G[u]) {
int v = e.first, w = e.second;
if (v == fat) continue;
fa[v][0] = u;
_for(i, 1, 18) fa[v][i] = fa[fa[v][i - 1]][i - 1];
dist[v] = dist[u] + w;
dfs(v, u, depth + 1);
}
}
int get_lca(int a, int b) {
if (dep[a] < dep[b]) swap(a, b);
_pfor(i, 18, 0) {
if (dep[fa[a][i]] < dep[b]) continue;
a = fa[a][i];
}
if (a == b) return a;
_pfor(i, 18, 0) {
if (fa[a][i] != fa[b][i]) {
a = fa[a][i];
b = fa[b][i];
}
}
A = a, B = b;
return fa[a][0];
}
signed main() {
cin >> n >> m >> q; IDX = n;
_for(i, 1, m) {
int u, v, w;
cin >> u >> v >> w;
add_edge(u, v, w);
add_edge(v, u, w);
}
tarjan(1, 0);
dfs(1, 0, 1);
while (q--) {
int u, v;
cin >> u >> v;
int p = get_lca(u, v);
if (p <= n) cout << dist[u] + dist[v] - dist[p] * 2 << endl;
else {
int da = dist[u] - dist[A], db = dist[v] - dist[B];
int l = abs(s[A] - s[B]);
cout << da + db + min(l, stot[A] - l) << endl;
}
}
}
例题 2:P4244 仙人掌求直径
考虑用树形 $dp$ 的思想来做本题,枚举每一个点作为树的最高点,此时经过该点的直径应该是该点往下能到达的最长路和次长路的和。然后统计所有点作为最高点的情况,取一个最大值就是直径。
如果当前枚举的点是圆点,这里我们在枚举往下走的路径时,枚举的两条路径一定属于不同环,则此时圆点的所有情况一定是能对应到原图中的。
如果当前枚举的点是方点,意味所在简单环的点,要任选两个点往下走一条路,在加上环上这两点距离就是答案。设 $d_i$ 表示 $i$ 往下走的距离,那么枚举 $i,j$ 则这条路径的长度就是 $d_i+d_j+(i-j)$。先破环成链(倍长),将这个看成 $d_i+i+d_j-j$,则 $d_i+i$ 固定,则要求 $d_j-j$ 最大,单调队列(队尾减队头不超过环长)即可完成。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e5 + 5;
int n, m, new_n, idx, fu[N], fw[N], f[N], s[N], stot[N], res, q[N];
int dfn[N], low[N], cnt, d[N];
struct node {
int v, w, id;
};
vector<node> G[N], nG[N];
void build_circle(int x, int y, int z) {
int sum = z;
for (int k = y; k != x; k = fu[k]) {
s[k] = sum;
sum += fw[k];
}
s[x] = stot[x] = sum;
nG[x].push_back({++new_n, 0, idx++});
for (int k = y; k != x; k = fu[k]) {
stot[k] = sum;
nG[new_n].push_back({k, min(s[k], sum - s[k]), idx++});
}
}
void tarjan(int u, int from) {
dfn[u] = low[u] = ++cnt;
for (auto e : G[u]) {
int v = e.v, w = e.w, id = e.id;
if (!dfn[v]) {
fu[v] = u, fw[v] = 1;
tarjan(v, id);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v]) nG[u].push_back({v, w, idx++});
}
else if (id != (from ^ 1)) low[u] = min(low[u], dfn[v]);
}
for (auto e : G[u]) {
int v = e.v, w = e.w, id = e.id;
if (dfn[u] < dfn[v] && fu[v] != u) {
build_circle(u, v, w);
}
}
}
int dfs(int u) {
int d1 = 0, d2 = 0;
for (auto e : nG[u]) {
int v = e.v, w = e.w, id = e.id;
int t = dfs(v) + w;
if (t > d1) d2 = d1, d1 = t;
else if (t > d2) d2 = t;
}
f[u] = d1;
if (u <= n) res = max(res, d1 + d2);
else {
int sz = 0;
for (auto e : nG[u]) {
int v = e.v;
d[++sz] = f[v];
}
d[++sz] = f[fu[u]];
for (int i = 1; i <= sz; i++) d[sz + i] = d[i];
int hd = 1, tl = 0;
for (int i = 1; i <= sz * 2; i++) {
while (hd <= tl && i - q[hd] > sz / 2) hd++;
if (hd <= tl) res = max(res, d[i] + d[q[hd]] + i - q[hd]);
while (hd <= tl && d[q[tl]] - q[tl] <= d[i] - i) tl--;
q[++tl] = i;
}
}
return f[u];
}
signed main() {
cin >> n >> m; new_n = n;
for (int i = 1; i <= m; i++) {
int k, x, y;
cin >> k >> x;
for (int j = 1; j <= k - 1; j++) {
cin >> y;
G[x].push_back({y, 1, idx++});
G[y].push_back({x, 1, idx++});
x = y;
}
}
tarjan(1, -1);
dfs(1);
cout << res << endl;
}
例题 3:BZOJ4316 仙人掌求最大独立集
按照常规建圆方树,只不过此时不用有边权。对于一个树边,按照常规树形 dp 转移。对于一个环,把环拆开。
比如一个环上点排成 $1\to 5\to 4\to 3 \to 2 \to 1$。对环上点进行 dp,环上点的点权为这个点的 $f_{u,0/1}$。
- 先确定 $1$ 号点不选,那么 $5$ 号点就没有限制。
- 再确定 $1$ 号点选,将 $5$ 号点选设置为非法。
具体代码:
void build_circle(int x, int y) {
// f用来辅助环上dp。当然可以用几个变量,只不过我看不懂
tot = 0;
for (int i = y; i != fa[x]; i = fa[i]) {
tot++;
f[tot][0] = dp[i][0], f[tot][1] = dp[i][1];
}
_for(i, 2, tot) {
f[i][0] += max(f[i - 1][0], f[i - 1][1]);
f[i][1] += f[i - 1][0];
}
dp[x][0] = f[tot][0]; // 先考虑不选
tot = 0;
for (int i = y; i != fa[x]; i = fa[i]) {
tot++;
f[tot][0] = dp[i][0], f[tot][1] = dp[i][1];
}
f[1][1] = -2e9;
_for(i, 2, tot) {
f[i][0] += max(f[i - 1][0], f[i - 1][1]);
f[i][1] += f[i - 1][0];
}
dp[x][1] = f[tot][1]; // 再考虑选
}
void tarjan(int u, int from) {
dfn[u] = low[u] = ++idx;
dp[u][1] = 1; dp[u][0] = 0;
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e;
if (!dfn[v]) {
fa[v] = u, fe[v] = p;
tarjan(v, p);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v]) {
dp[u][0] += max(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
else if (p != (from ^ 1)) low[u] = min(low[u], dfn[v]);
}
for (int p = first[u]; p; p = ed[p].next) {
int v = ed[p].e;
if (fe[v] != p && dfn[u] < dfn[v]) build_circle(u, v);
}
}
signed main() {
cin >> n >> m;
_for(i, 1, m) {
int u, v;
cin >> u >> v;
add_edge(u, v);
add_edge(v, u);
}
tarjan(1, 0);
cout << max(dp[1][0], dp[1][1]) << endl;
}
双倍经验:P4410 无归岛。
广义圆方树
这个东西就大为不同,我们只需要用点双就行了,而且是双向。下面的图显示了一张图对应的点双和广义圆方树形态。
这个东西的建立也很简单,我们每找到一个点双连通分量,就建立出一个方点,向这个点双连通分量中每个点连一条无向边。下面是建立方式:
void tarjan(int u) {
dfn[u] = low[u] = ++idx;
stk.push(u);
for (auto v : G[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v]) {
num++;
int y;
do {
y = stk.top(); stk.pop();
nG[num].push_back(y);
nG[y].push_back(num);
} while (y != v);
nG[num].push_back(u);
nG[u].push_back(num);
}
}
else low[u] = min(low[u], dfn[v]);
}
}
其实这个东西和点双缩点是互通的,点双如何缩点可以看 这篇博客(不是我写的,仅为引用)。
模板题:ABC318G
题意
给出一个有 $n$ 个顶点和 $m$ 条边的无向连通图 $G$,没有重边和自环。
顶点的编号为 $1 \sim n$,边的编号为 $1 \sim m$,第 $i$ 条边连接顶点 $u_i$ 和 $v_i$。
给出图上三个不同的顶点 $A,B,C$。判断是否有从点 $A$ 经过点 $B$ 到点 $C$ 的简单路径。
简单路径指路径上的点互不相同,即不重复经过同一个点。
思路
先建广义圆方树。有一个性质:对于一个点双中的两点,它们之间简单路径的并集,恰好完全等于这个点双。
写就是说:两圆点在圆方树上的路径,与路径上经过的方点相邻的圆点的集合,就等于原图中两点简单路径上的点集。证明可以用网络流。
那么 $A,C$ 在圆方树路径上:
- 若存在点 $B$,则有解。
- 否则,对于路径上每一个方点相邻的圆点中存在点 $B$,也有解。
例题 $1$:P4630 三点形成的简单路径计数
根据模板题,我们已经可以求出:给定两点,求两点简单路径上的点集的大小。将圆点权值设置为 $-1$,将方点权值设置为相邻原点的个数。则两点间在园方树上路径的权值和就是点集大小,并且这样可以顺便减去 $2$(不包含 $s,f$)。
可以直接 $O(n^2)$ 。但是需要优化。换个角度考虑,改为统计每一个点对答案的贡献,即权值乘以经过它的路径条数,这可以通过简单的树上计数得到。
例题 $2$:P4606
题意
给一张连通图,给一图中点集 $S$,求这张图上有多少个点(不包含点集中的点)满足删去这个点后,给的点集 $|S|$ 不全在一个连通分量中。
思路
先建出圆方树。
如果点集大小只有 $2$,答案就是路径上圆点的个数。如果点集大于 $2$,就是点集形成的极小连通子图中的圆点个数。如何求解,有一个 trick:
首先将圆点的权值转化为边权权值,即把圆点变成圆点到它父亲边上的权值。将给定点集按照 dfn 序排序,计算排序后相邻两点的边权距离和,答案就是距离和的一半,因为每条边只被经过两次。
可写成:$\dfrac{dis(S_1,S_2)+dis(S_2,S_3)+\dots+dis(S_{n-1},S_n)+dis(S_n,S_1)}{2}$。
注意了,只有边权距离和才能满足这个性质,点权是不行的。所以我们最开始要将点权转边权。
当 $lca(S_1,S_n)$ 是圆点时,边权距离是算不到这个最高点的,所以此时答案加一。
例题 $3$:CF487E:
题意
给定一个图,图上每个点有点权。要处理 $q$ 个操作: C a w:表示 $a$ 点的点权变成 $w$。 A a b:表示有一个游客要从 $a$ 点到 $b$ 点,你要回答在两点间所有简单路径中最低点权的最低可能值。
题意
看到简单路径,立刻应该想到点双,圆方树。
根据之前模板题的性质,令方点权值为相邻圆点权值的最小值,问题转化为求路径上最小值。
但是这样遇到一个问题,如果修改一个点权,这个点周围的方点需要全部修改,一次修改最多 $O(n)$ 复杂度,很容易被卡。
利用圆方树是一颗树的性质,对于一个方点,我们只需要维护其所有儿子圆点的权值最小值。这样查询路径上最小值还是正确的。只有一个潜在问题,如果两个点的 LCA 是方点,那么需要额外加上方点的父亲的权值。可以画图理解。
所以修改一个圆点只需要将圆点父亲的方点修改就行,儿子方点不用管。对每个方点开一个 multiset,维护所有儿子圆点的权值即可。
CF 数据非常水,用 set 都能过!!所以建议交 uoj。代码
感谢大家支持!像我这么帅气的人