圆方树学习笔记 & 最短路 题解
前言
圆方树学习笔记,从一道例题讲起。
题目链接: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 的过程。
发现,只会存在花瓣到花心,再到花顶端的那个花瓣。我们不妨设花心到顶端花瓣的距离为
至于 LCA 处,也很简单。如果 LCA 是圆点,那么不用处理;反之是方点,就要算其对应的原图上环的两点间的距离,这两点是来自询问两点不超过 LCA 的最浅祖先。这很好理解。
其实分析到这里,代码已经可以打出来了,但是在建树的时候注意如何优雅地处理边权,并精简代码。
那么再来模一模样例加深印象。
这里加粗的是原图的圆点,其他是方点。左图是圆仙人掌,右图是建出来的圆方树。
代码
略去了快读快写,使用树剖求树上距离、LCA、和跳 father。
时间复杂度:
#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; }
后记
不用圆方树亦可解此题,但是要多一些讨论、不如圆方树的直观。据说圆方树能在一般无向图上使用?我太蒻了啊。
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18313014。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现