仙人掌&圆方树
upd:过完年回来惊闻仙人掌在OI考纲上被咔嚓了,所以这篇文章中仙人掌的部分可以跳过或者略读,但是用于处理无向图的(广义)圆方树还是建议有一定基础的读者详细了解。
仙人掌&圆方树
仙人掌是一种每条边最多出现在一个环中的无向连通图我也不知道为什么要叫仙人掌。
对于每一个环,如果我们删去一条边就得到了一棵树;如果把每个环缩成一个点也会得到一棵树。这使得仙人掌在许多方面具有与树相似的性质,处理仙人掌上的问题时,我们常常运用与树相似的思想来处理。
例如下面这道例题:
题意就是求仙人掌的最大独立集,即在图中选择最多的点使得这些点互相不联通。
考虑一棵树的情况怎么做,设\(f[i][0/1]\)表示\(i\)子树内不选/选\(i\)的最大独立集,设\(j\)是\(i\)的儿子,转移就是:
考虑仙人掌上的转移,对于树边和非树边(称不在环上的边为树边)分类讨论一波:如果连接父子的边是树边,那么直接转移,如果发现某个点是某个环的最先被访问到的点,那么把这个环拎出来,考虑这个点选或不选:如果选,那么就强制最后被访问到的点不选,如果不选则不强制。我们可以tarjan实现这个过程,复杂度线性,详见下面的代码。
事实上,此时我们\(f\)数组的意义已经发生了一些变化,\(f[i][0/1]\)表示这个点子仙人掌内的最大独立集:对于不在环中的点,这个数组的定义和树上差不多;对于在环上且不是这个环中最先被访问到的点,这个数组是忽略了环上的其他点对这个点的影响的;对于某个环中最先被访问到的点,这个数组包含了这个环内所有点的影响。其实这就反映了仙人掌和树的相似点和不同之处,在仙人掌上DP时,一般是首先考虑树上的做法,然后考虑上面所说的后两类点怎样处理。
#include <cstdio>
#include <cctype>
#include <vector>
#define R register
#define I inline
#define B 1000000
using namespace std;
const int N = 50003, inf = 1e9;
char buf[B], *p1, *p2;
I char gc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, B, stdin), p1 == p2) ? EOF : *p1++; }
I int rd() {
R int f = 0;
R char c = gc();
while (c < 48 || c > 57)
c = gc();
while (c > 47 && c < 58)
f = f * 10 + (c ^ 48), c = gc();
return f;
}
int s[N], f[N][2], p[N], d[N], l[N], t;
vector <int> g[N];
I int min(int x, int y) { return x < y ? x : y; }
I int max(int x, int y) { return x > y ? x : y; }
void dfs(int x, int h) {
p[x] = h, d[x] = l[x] = ++t, f[x][1] = 1, f[x][0] = 0;
R int i ,y;
for (i = 0; i < s[x]; ++i) {
if (!d[y = g[x][i]])
dfs(y, x), l[x] = min(l[x], l[y]);
else
if (y ^ h)
l[x] = min(l[x], d[y]);
if(l[y] > d[x])
f[x][1] += f[y][0], f[x][0] += max(f[y][0], f[y][1]);
}
for (i = 0; i < s[x]; ++i)
if (p[y = g[x][i]] ^ x && d[x] < d[y]) {
R int j, u[2], v[2];
v[0] = v[1] = 0;
for (j = y; j ^ x; j = p[j])
u[0] = v[0] + f[j][0], u[1] = v[1] + f[j][1], v[0] = max(u[0], u[1]), v[1] = u[0];
f[x][0] += v[0], v[0] = 0, v[1] = -inf;
for (j = y; j ^ x; j = p[j])
u[0] = v[0] + f[j][0], u[1] = v[1] + f[j][1], v[0] = max(u[0], u[1]), v[1] = u[0];
f[x][1] += v[1];
}
}
int main() {
R int n = rd(), m = rd(), i, x, y;
for (i = 1; i <= m; ++i)
x = rd(), y = rd(), g[x].push_back(y), g[y].push_back(x);
for (i = 1; i <= n; ++i)
s[i] = g[i].size();
dfs(1, 0), printf("%d", max(f[1][0], f[1][1]));
return 0;
}
再给一道在仙人掌上DP的例题:[SHOI2008]仙人掌图 题解
上面给出的题目是直接在仙人掌上DP,但有时候我们要执行更多的操作,就需要使用一个处理仙人掌问题的利器——圆方树。
还是给出例题:BZOJ2125 最短路
这题是求仙人掌上两点距离,考虑我们在树上是怎样做的:设\(dis[i]\)表示\(i\)到根的距离,询问时求lca,答案就是\(dis[i] + dis[j] - dis[lca] * 2\)。
放到仙人掌上之后,考虑建出圆方树:定义圆点为原图中存在的点,方点是我们构造出的点,每一个方点连接原图中同一个环中的点。考虑这样建出的东西具有什么性质:
- 显然是一棵树;
- 方点和方点不会直接相连;
- 无论以哪个点为根构建圆方树,建出的树形态都是一样的;
- 原图的联通关系没有改变;
- 两个圆点之间的路径是原图中两点的简单路径的并(这一条性质在广义圆方树上同样适用,并且非常有用)。
这样我们就可以借助圆方树,把仙人掌上的问题化为树上问题来解决了。
首先一遍tarjan建出圆方树。圆圆边的边权不变, 令圆方边的边权为圆点到这个点所在环的最顶端的点的最短距离。如果询问的两个点在圆方树上的lca是圆点,就直接和树上一样的做法;如果lca是方点,说明这两个点在原来的仙人掌上的最短路径相交于lca所在的那个环上,可以在环上找最短路径,需要维护环大小和从环顶到某个点是否经过返祖边,现在考虑怎样跳到环上,因为我习惯用树剖求lca,所以就讲树剖的做法:目标点是lca的儿子,如果是重儿子,dfs序就是lca的dfs序\(+1\),如果是轻儿子,就一定是一条重链的顶端,那么我们就直接跳重链,跳的时候记录一下是从哪里跳过来的就行了。
#include <cstdio>
#include <cctype>
#include <vector>
#include <algorithm>
#define R register
#define I inline
#define B 1000000
using namespace std;
const int N = 200003;
char buf[B], *p1, *p2;
I char gc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, B, stdin), p1 == p2) ? EOF : *p1++; }
I int rd() {
R int f = 0;
R char c = gc();
while (c < 48 || c > 57)
c = gc();
while (c > 47 && c < 58)
f = f * 10 + (c ^ 48), c = gc();
return f;
}
vector <pair<int, int> > g[N], G[N];
int s[N], S[N], fa[N], dfn[N], low[N], dep[N], sta[N], cir[N], hal[N], dis[N], n, tim, cnt;
I int min(int x, int y) { return x < y ? x : y; }
I int abs(int x) { return x >= 0 ? x : ~x + 1; }
I void build(int x, int y, int z) {
R int top = dep[y] - dep[x] + 1, sum = z + dis[y] - dis[x], d = 0, D, i;
for (i = y; i ^ x; i = fa[i])
sta[top--] = i;
cir[++cnt] = sum, sta[1] = x, top = dep[y] - dep[x] + 1, G[x].push_back(make_pair(cnt, 0));
for (i = 2; i <= top; ++i)
d += dis[sta[i]] - dis[sta[i - 1]], D = min(d, sum - d), G[cnt].push_back(make_pair(sta[i], D)), hal[sta[i]] = d ^ D;
}
void dfs(int x, int f) {
dfn[x] = low[x] = ++tim, dep[x] = dep[f] + 1, fa[x] = f;
R int i, y;
for (i = 0; i < s[x]; ++i) {
if(!dfn[y = g[x][i].first])
dis[y] = dis[x] + g[x][i].second, dfs(y, x), low[x] = min(low[x], low[y]);
else
if (y ^ f)
low[x] = min(low[x], dfn[y]);
if (dfn[x] < low[y])
G[x].push_back(make_pair(y, g[x][i].second));
}
for (i = 0; i < s[x]; ++i)
if (fa[y = g[x][i].first] ^ x && dfn[x] < dfn[y])
build(x, y, g[x][i].second);
}
int son[N], tot[N], top[N], f[N];
void dfs1(int x, int f) {
fa[x] = f, dep[x] = dep[f] + 1, tot[x] = 1;
for (R int i = 0, y, m = 0; i < S[x]; ++i)
if ((y = G[x][i].first) ^ f) {
dis[y] = dis[x] + G[x][i].second, dfs1(y, x), tot[x] += tot[y];
if (tot[y] > m)
m = tot[y], son[x] = y;
}
}
void dfs2(int x, int r) {
top[x] = r, dfn[x] = ++tim, f[tim] = x;
if (son[x])
dfs2(son[x], r);
for (R int i = 0, y; i < S[x]; ++i)
if ((y = G[x][i].first) ^ fa[x] && y ^ son[x])
dfs2(y, y);
}
I int lca(int x, int y) {
while (top[x] ^ top[y])
dep[top[x]] > dep[top[y]] ? x = fa[top[x]] : y = fa[top[y]];
return dep[x] < dep[y] ? x : y;
}
I int jump(int x, int y) {
R int tmp;
while (top[x] ^ top[y])
tmp = top[x], x = fa[tmp];
return x == y ? tmp : f[dfn[y] + 1];
}
I int query(int x, int y) {
R int z = lca(x, y), u, v, dx, dy;
if (z <= n)
return dis[x] + dis[y] - (dis[z] << 1);
u = jump(x, z), v = jump(y, z), dx = dis[u] - dis[z], dy = dis[v] - dis[z];
if (hal[u])
dx = cir[z] - dx;
if (hal[v])
dy = cir[z] - dy;
dx = abs(dx - dy);
return dis[x] - dis[u] + dis[y] - dis[v] + min(dx, cir[z] - dx);
}
int main() {
R int m, Q, i, x, y, z;
cnt = n = rd(), m = rd(), Q = rd();
for (i = 1; i <= m; ++i)
x = rd(), y = rd(), z = rd(), g[x].push_back(make_pair(y, z)), g[y].push_back(make_pair(x, z));
for (i = 1; i <= n; ++i)
s[i] = g[i].size();
dfs(1, 0), tim = 0;
for (i = 1; i <= cnt; ++i)
S[i] = G[i].size();
dfs1(1, 0), dfs2(1, 1);
for (i = 1; i <= Q; ++i)
x = rd(), y = rd(), printf("%d\n", query(x, y));
return 0;
}
通过上面这道例题可以看出,圆方树和在仙人掌上DP的思想其实还是一样的,只是为了能够让一些在树上适用的数据结构能在仙人掌上使用,于是完整地建出了一棵树来方便我们处理。
事实上,圆方树能处理的问题还远不止于仙人掌上,有些情况下还能处理一般的无向图,我们也称这一类圆方树为广义圆方树。还是看道例题吧:CF487E Tourists
简述下题意:求无向图两点间所有简单路径上权值最小的点,点权带修改。
注意到这题的限制“简单路径”,想想要求走过的路是简单路径意味着什么:每一个点双连通分量中的点都具有比其他点更强的关系,只要到了其中任意一个点,就一定可以到达这个点双内的具有最小权值的点,并从某一条边走出这个点双。考虑用圆方树来表达这一关系:对每一个点双建一个方点连接点双内的所有点,注意点双连通分量都是极大的。发现了什么?原图中重要的关系被完美地保留了下来,而且现在留给我们处理的东西变成了一棵树,不仅如此,这棵树还具有一个非常好的性质:圆点和方点是相间分布的(可以自己手玩)。这下我们可以考虑怎样在建出来的树上维护原图的信息了,注意上面提到的几点性质,下面会要用到:令每个方点的权值为它周围所有圆点权值的最小值,这样我们就可以树剖+线段树解决了,具体来说就对每个方点开个multiset,然后。。。等等,这样做要是一朵菊花不就卡死了吗?最后的操作:可以看出我们已经把这棵树变成了有根树,对于每个方点只要维护它儿子的信息就行了,修改圆点权值的时候就只要更新它父亲的权值,查询的时候如果lca是方点,就用他父亲的权值更新一下答案就好了。
#include <cstdio>
#include <vector>
#include <set>
#define R register
#define I inline
using namespace std;
const int N = 200003, inf = 0x7fffffff;
int s[N], S[N], w[N], vis[N], dfn[N], low[N], sta[N], fa[N], dep[N], tot[N], son[N], top[N], u[N], v[N << 2], n, tim, cnt, stp;
vector <int> g[N], G[N];
multiset <int> mset[N];
I int min(int x, int y) { return x < y ? x : y; }
I void swap(int &x, int &y) { x ^= y, y ^= x, x ^= y; }
I void pushup(int k, int p, int q) { v[k] = min(v[p], v[q]); }
void dfs(int x) {
vis[sta[++stp] = x] = 1, low[x] = dfn[x] = ++tim;
R int i, y, z;
for (i = 0; i < s[x]; ++i)
if (!dfn[y = g[x][i]]) {
dfs(y), low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x]) {
++cnt, w[cnt] = min(w[cnt], w[x]), G[cnt].push_back(x), G[x].push_back(cnt);
do {
vis[z = sta[stp--]] = 0, G[cnt].push_back(z), G[z].push_back(cnt);
} while (z ^ y);
}
}
else
low[x] = min(low[x], dfn[y]);
}
void dfs1(int x, int f) {
fa[x] = f, dep[x] = dep[f] + 1, tot[x] = 1;
for (R int i = 0, y, m = 0; i < S[x]; ++i)
if ((y = G[x][i]) ^ f) {
dfs1(y, x), tot[x] += tot[y];
if (tot[y] > m)
m = tot[y], son[x] = y;
}
}
void dfs2(int x, int r) {
dfn[x] = ++tim, top[x] = r;
if (son[x])
dfs2(son[x], r);
for (R int i = 0, y; i < S[x]; ++i)
if ((y = G[x][i]) ^ fa[x] && y ^ son[x])
dfs2(y, y);
}
void build(int k, int l, int r) {
if (l == r) {
v[k] = u[l];
return ;
}
R int p = k << 1, q = p | 1, m = l + r >> 1;
build(p, l, m), build(q, m + 1, r), pushup(k, p, q);
}
void tmodify(int k, int l, int r, int x, int y) {
if (l == r) {
v[k] = y;
return ;
}
R int p = k << 1, q = p | 1, m = l + r >> 1;
if (x <= m)
tmodify(p, l, m, x, y);
else
tmodify(q, m + 1, r, x, y);
pushup(k, p, q);
}
int tquery(int k, int l, int r, int x, int y) {
if (x <= l && r <= y)
return v[k];
R int p = k << 1, q = p | 1, m = l + r >> 1, o = inf;
if (x <= m)
o = min(o, tquery(p, l, m, x, y));
if (m < y)
o = min(o, tquery(q, m + 1, r, x, y));
return o;
}
I void modify(int x, int y) {
R int f = fa[x];
if (f) {
mset[f].erase(mset[f].find(w[x]));
mset[f].insert(y);
w[f] = *mset[f].begin();
tmodify(1, 1, cnt, dfn[f], w[f]);
}
w[x] = y, tmodify(1, 1, cnt, dfn[x], y);
}
I int query(int x, int y) {
R int o = inf;
while (top[x] ^ top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
o = min(o, tquery(1, 1, cnt, dfn[top[x]], dfn[x])), x = fa[top[x]];
}
if (dep[x] > dep[y])
swap(x, y);
o = min(o, tquery(1, 1, cnt, dfn[x], dfn[y]));
if (x > n)
o = min(o, w[fa[x]]);
return o;
}
int main() {
R int m, Q, i, x, y;
R char o[2];
scanf("%d%d%d", &n, &m, &Q), cnt = n;
for (i = 1; i <= n; ++i)
scanf("%d", &w[i]);
for (i = 1; i <= m; ++i)
scanf("%d%d", &x, &y), g[x].push_back(y), g[y].push_back(x);
for (i = 1; i <= n; ++i)
s[i] = g[i].size();
dfs(1), tim = 0;
for (i = 1; i <= cnt; ++i)
S[i] = G[i].size();
dfs1(1, 0), dfs2(1, 1);
for (i = 1; i <= n; ++i)
u[dfn[i]] = w[i];
for (x = n + 1; x <= cnt; ++x) {
for (i = 0; i < S[x]; ++i)
if ((y = G[x][i]) ^ fa[x])
mset[x].insert(w[y]);
w[x] = u[dfn[x]] = mset[x].empty() ? inf : *mset[x].begin();
}
build(1, 1, cnt);
for (i = 1; i <= Q; ++i) {
scanf("%s%d%d", o, &x, &y);
if (o[0] == 'A')
printf("%d\n", query(x, y));
else
modify(x, y);
}
return 0;
}
丢几道题吧:
P4320 道路相遇 题解
[SDOI2018]战略游戏 题解
[APIO2018] Duathlon 铁人两项 题解
呼。。。完结撒花。
鸣谢:orz @yybyyb