(广义)圆方树
圆方树用于解决仙人掌(没有一条边在两个环上的无向连通图)上的一些问题,广义圆方树用于解决无向图中的一些问题。(不过目前没怎么做过题,不知道广义不广义有啥区别)
前置技能
tarjan 算法求点双
一定要先学好 tarjan 算法啊!! 吃够了基础不牢的苦
圆方树的构建
和 tarjan 点双缩点不一样的是,我们并不去“复制”割点,甚至都不用显式地标出割点,我们直接用一个“方点”代表一个DCC,“方点”向DCC上的所有点连边,以保证每个点只出现一次。特别的,当DCC中只有两个点的时候,我们更倾向于不建“方点”,因为这种情况不会出环,只是个没有割点的DCC
至于边权,就可能是具体问题具体分析了。(因为我目前就做对过一道题)。比如P5236 【模板】静态仙人掌
这道例题,求仙人掌上两点最短路,我们的处理方法是:圆圆边(圆点-圆点)边权就是原图边权;圆方边边权是圆点到方点的父亲(即DCC里面靠上的那个割点)在原图中的最短距离。查询的话用树剖lca的方法解决,如果lca是圆点,就照常做;如果是方点,就要把经过那个环的两种路径都考虑一下。
总之,细节很多,需要一些技巧。不过如果不考虑边权的话,圆方树还是比较友好的。
代码:(比较复杂)
void tarjan(int cur) {
dfn[cur] = low[cur] = ++dcnt;
stk[++stop] = cur;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
pre_val[to] = e[i].val;
tarjan(to);
MIN(low[cur], low[to]);
if (low[to] >= dfn[cur]) {
if (stk[stop - 1] == cur) {
--stop;
Addedge(cur, to, e[i].val);
Addedge(to, cur, e[i].val);
continue;
}
++dtot;
int p = stk[stop], S = lval[p];
for (register int j = stop; stk[j] != cur; --j)
sum[stk[j]] = S, S += pre_val[stk[j]];
while (stk[stop] != cur) {//注意为了适应广义圆方树,这里最好也写成 lst != to
int tmp = stk[stop--];
int evl = min(sum[tmp], S - sum[tmp]);
Addedge(tmp, dtot, evl);
Addedge(dtot, tmp, evl);
}
Addedge(cur, dtot, 0), Addedge(dtot, cur, 0); sum[cur] = 0;
sum[dtot] = S;
}
} else {
if (dfn[to] < low[cur]) low[cur] = dfn[to], lval[cur] = e[i].val;
}
}
}
int Find(int x, int lca) {//找到 x - lca 路径中lca下面的那个点的编号
int lst = 0;
while (top[x] != top[lca]) lst = top[x], x = fa[top[x]];
return x == lca ? lst : son[lca];
}
...
int main() {
...
int lca = get_lca(u, v);
if (lca <= n)
printf("%d\n", dis[u] + dis[v] - (dis[lca] << 1));
else {
int ans = dis[u] + dis[v];
u = Find(u, lca), v = Find(v, lca);
ans = ans - dis[u] - dis[v];
if (sum[u] < sum[v]) swap(u, v);
ans += min(sum[u] - sum[v], sum[lca] - (sum[u] - sum[v]));
printf("%d\n", ans);
}
...
}
例题
tourists
给一张简单无向连通图,要求支持单点修改点权和查询 \(x\) 到 \(y\) 的所有简单路径上的点权的最小值。\(n,m,q \le 10^5\)
简单路径的定义为不经过重点,于是可以想到点双缩点或者广义圆方树。
建出圆方树(或点双缩点成树),\(x \to y\) 的所有路径上的点的最小值实际上就是链上的点(dcc)的最小值。于是可以将方点点权设为该dcc上的点权最小值,树剖查询即可。
现还要求支持单点修改。我们每修改一个点的点权后,还要相应地修改所有与其相邻的方点的点权。暴力修改显然复杂度是错的,于是可以使用 Fusion tree 的经典套路,我们只将点的贡献计算给其父亲,这样的话方点会拥有所有儿子的信息,缺失父亲的信息,用的时候查一下即可。每次只有 lca 可能会确实信息,于是复杂度是正确的。
点双的做法对于dcc维护最小值,将割点及dcc内的非割点的信息统计好。非割点的复杂度没问题,割点的方法与圆方树的方法类似。
战略游戏
没时间写了,以后再说。
模板(调试用)
//tourists
int dfn[N], dcnt, low[N];
int stk[N], stop;
void tarjan(int cur) {
dfn[cur] = low[cur] = ++dcnt;
stk[++stop] = cur;
for (int i= head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to); MIN(low[cur], low[to]);
if (low[to] >= dfn[cur]) {
++ptot;
int tmp;
do {
tmp = stk[stop--];
E[++Ecnt] = (Edge){tmp, ptot};
} while (tmp != to);
E[++Ecnt] = (Edge){cur, ptot};
}
} else MIN(low[cur], dfn[to]);
}
}