圆方树学习笔记 & 最短路 题解

前言

圆方树学习笔记,从一道例题讲起。

题目链接:Hydro & bzoj

题意简述

仙人掌上求两点距离。

题目分析

为了把仙人掌的性质发挥出来,考虑将其变成一棵树。圆方树就是这样转换的工具。

先讲讲圆方树的概念:原图上的点为圆点,每个点双对应一个方点,树边都是方点连向点双内的圆点。

具体代码实现也十分简单,就是在 tarjan 求点双的时候,弹栈的时候,新建结点,并和点双内的点连边即可。或者在处理最后连边即可。

注意,对于孤立点的处理,是保持原样不动,还是变为一对方点和圆点,视情况讨论。以下代码按照上述定义,即对于孤立点,在圆方树上体现为一对方点和白点。

void tarjan(int now, int fa){
dfn[now] = low[now] = ++timer, stack[++top] = now;
int son = 0;
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (!dfn[to]){
tarjan(to, now), low[now] = min(low[now], low[to]), ++son;
if (low[to] >= dfn[now]){
scc[++scc_cnt].push_back(now);
do scc[scc_cnt].push_back(stack[top--]); while (stack[top + 1] != to);
}
} else if (to != fa) low[now] = min(low[now], dfn[to]);
}
if (!son && !fa) scc[++scc_cnt].push_back(now);
}
for (int i = 1; i <= scc_cnt; ++i)
for (const auto& u: scc[i]) {
yzh.add(i + n, u);
yzh.add(u, i + n);
}
void tarjan(int now, int fa){
dfn[now] = low[now] = ++timer, stack[++top] = now;
int son = 0;
for (int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].to;
if (!dfn[to]){
tarjan(to, now), low[now] = min(low[now], low[to]), ++son;
if (low[to] >= dfn[now]){
++scc_cnt;
yzh.add(now, scc_cnt + n);
yzh.add(scc_cnt + n, now);
do {
int u = stack[top--];
yzh.add(u, scc_cnt + n);
yzh.add(scc_cnt + n, u);
} while (stack[top + 1] != to);
}
} else if (to != fa) low[now] = min(low[now], dfn[to]);
}
if (!son && !fa) {
++scc_cnt;
yzh.add(now, scc_cnt + n);
yzh.add(scc_cnt + n, now);
}
}

注意到,有时候,我们需要根据题意记录树边的边权。就比如例题。

思考我们建出树的意义是什么——原仙人掌上,不好求两点间的距离,而在树上,我们可以轻松利用 LCA 等方法求得两点间的距离。所以树的边权应该和距离有关。

我们发现,仙人掌中,原来的环都变成了菊花。原图上的距离是环上的最短距离,而在树上是从花瓣到花心,再从花心到花瓣。这样似乎没看出些什么。我们不妨把比较特殊的 LCA 处放在最后讨论,接下来只考虑往上跳 LCA 的过程。

发现,只会存在花瓣到花心,再到花顶端的那个花瓣。我们不妨设花心到顶端花瓣的距离为 0,而其余花瓣到花心的距离设置成原图环上到顶点的距离。这样就可以很好转化边权了。

至于 LCA 处,也很简单。如果 LCA 是圆点,那么不用处理;反之是方点,就要算其对应的原图上环的两点间的距离,这两点是来自询问两点不超过 LCA 的最浅祖先。这很好理解。

其实分析到这里,代码已经可以打出来了,但是在建树的时候注意如何优雅地处理边权,并精简代码。

那么再来模一模样例加深印象。

这里加粗的是原图的圆点,其他是方点。左图是圆仙人掌,右图是建出来的圆方树。

dist(5,7):由于 LCA(5,7)=1 是圆点,所以直接路径和相加,即 dist(5,7)=3+0+1+0+2+0=6

dist(4,8):由于 LCA(4,8)=14 是方点,所以要先跳到差一步到 14 的地方,即 43,环上距离为 min{1,41}=1dist(4,8)=2+0+1+0+1=4

代码

略去了快读快写,使用树剖求树上距离、LCA、和跳 father。

时间复杂度:Θ(n+m+qlogn),瓶颈在树剖查询的 log

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
struct Graph{
struct node{
int to, nxt, len;
} edge[1000010 << 1];
int eid = 1, head[20010];
inline void add(int u, int v, int w){
edge[++eid] = {v, head[u], w};
head[u] = eid;
}
inline node & operator [] (const int x){
return edge[x];
}
} xym, yzh;
int n, m, q;
int dfn[20010], low[20010], timer, cnt;
int dis[20010], re[20010];
int sum[20010], stack[20010], stop;
bool onleft[20010];
void tarjan(int now, int fr) {
dfn[now] = low[now] = ++timer, stack[++stop] = now;
for (int i = xym.head[now], to; to = xym[i].to, i; i = xym[i].nxt) if (i ^ fr ^ 1) {
if (!dfn[to]) {
dis[to] = dis[now] + xym[i].len;
re[to] = xym[i].len; // 如果这是一条非环边,那么把它看做 now -> to -> now 的环,所以这里要设置初值
tarjan(to, i), low[now] = min(low[now], low[to]);
if (low[to] >= dfn[now]) {
++cnt, sum[cnt] = re[stack[stop]] + dis[stack[stop]] - dis[now]; // 记录环的总长
yzh.add(cnt, now, 0), yzh.add(now, cnt, 0);
do {
int u = stack[stop--];
int len = min(dis[u] - dis[now], sum[cnt] - dis[u] + dis[now]);
onleft[u] = len == dis[u] - dis[now];
yzh.add(u, cnt, len);
yzh.add(cnt, u, len);
} while (stack[stop + 1] != to);
}
} else if (dfn[to] < dfn[now]) {
re[now] = xym[i].len; // 环的闭环那条边的长度
low[now] = min(low[now], dfn[to]);
}
}
}
int siz[20010], top[20010], son[20010];
int line[20010], tfa[20010], dpt[20010];
void dfs(int now, int fa) {
tfa[now] = fa, siz[now] = 1, dpt[now] = dpt[fa] + 1;
for (int i = yzh.head[now], to; to = yzh[i].to, i; i = yzh[i].nxt) {
if (to == fa) continue;
dis[to] = dis[now] + yzh[i].len;
dfs(to, now), siz[now] += siz[to];
if (siz[to] > siz[son[now]]) son[now] = to;
}
}
void redfs(int now, int tp) {
line[dfn[now] = ++timer] = now;
top[now] = tp;
if (son[now]) redfs(son[now], tp);
for (int i = yzh.head[now], to; to = yzh[i].to, i; i = yzh[i].nxt) {
if (to == tfa[now]) continue;
if (to == son[now]) continue;
redfs(to, to);
}
}
inline int lca(int u, int v) {
while (top[u] != top[v]) {
if (dpt[top[u]] < dpt[top[v]]) swap(u, v);
u = tfa[top[u]];
}
if (dpt[u] < dpt[v]) swap(u, v);
return v;
}
inline int jump(int now, int ed) {
int res = 0;
while (top[now] != top[ed])
res = top[now], now = tfa[top[now]];
return now == ed ? res : line[dfn[ed] + 1];
}
inline int query(int u, int v) {
int p = lca(u, v);
if (p <= n)
return dis[u] + dis[v] - 2 * dis[p];
int fu = jump(u, p), fv = jump(v, p);
int d1 = dis[fu] - dis[p], d2 = dis[fv] - dis[p];
if (!onleft[fu]) d1 = sum[p] - d1;
if (!onleft[fv]) d2 = sum[p] - d2;
return dis[u] - dis[fu] + dis[v] - dis[fv] + min(abs(d1 - d2), sum[p] - abs(d1 - d2));
}
signed main() {
fread(buf, 1, MAX, stdin);
read(n), read(m), read(q);
for (int i = 1, u, v, w; i <= m; ++i) {
read(u), read(v), read(w);
xym.add(u, v, w);
xym.add(v, u, w);
}
cnt = n;
for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i, 0);
timer = dis[1] = 0, dfs(1, 0), redfs(1, 1);
for (int i = 1, u, v; i <= q; ++i) {
read(u), read(v);
write(query(u, v)), putchar('\n');
}
fwrite(obuf, 1, o - obuf, stdout);
return 0;
}

后记

不用圆方树亦可解此题,但是要多一些讨论、不如圆方树的直观。据说圆方树能在一般无向图上使用?我太蒻了啊。

posted @   XuYueming  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示