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

前言

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

题目链接: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 的最浅祖先。这很好理解。

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

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

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

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

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

代码

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

时间复杂度:\(\Theta(n + m + q \log n)\),瓶颈在树剖查询的 \(\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 @ 2024-07-21 16:35  XuYueming  阅读(5)  评论(0编辑  收藏  举报