洛谷 P5236 【模板】静态仙人掌 题解
P5236 【模板】静态仙人掌
前言
我认为仙人掌是比较综合的题,涉及到了边双连通分量、最近公共祖先(甚至有些题目会用到树链剖分、动态树等等)。
所以呢,请先把下面的题掌握了再看此题
圆方树
因为仙人掌是有环的,这就导致求最短路的时候及其不方便,那么不妨将仙人掌做一个变形,变成一棵树,然后再进行操作,似乎就可做了。
这里引进“圆方树”,正如它的名字,圆方树中有“圆点”和“方点”两种点。圆方树该怎么建?看下面的图
你会发现一些有趣的性质:
- 圆方树的圆点 = 仙人掌的总点数
- 圆方树的方点 = 仙人掌的总环数
那么,圆方树具体是怎么建立的呢?
- 选取任意一个点为圆方树的跟。
- 如果该点在某一个环上,那么建立一个新的方点,并使该点向方点连一条权值为 0 的有向边,并把该点叫做这个环的“头”,再将环上其他点变成圆点,从方点依次向它们连一条权值为 k 的有向边。(k是什么详见下文)
- 如果该点不在某一个环上,那么建立若干个新的圆点,并使该点向新圆点连一条权值为 k 的有向边,这些新的圆点的数量是还没有遍历过的且与该点相邻的点的数量(其实说白了就是形态不变,从仙人掌那里复制过来建到圆方树里)
- 将这些新建的点再执行第 2 步,直到所有点均被便利。
k 表示:仙人掌中该点到这个点所在环的“头”的最短距离。
如何维护信息
如果我们在一颗普通的树上要求最短路,考虑用树上差分,维护每个点到树根的距离,然后对于两个树上点 \(a,b\) 的距离,求出它们的最近公共祖先 \(p\),然后答案为 \(dist[a]+dist[b]-2dist[p]\)。
不过圆方树并不是这么单纯的,它需要进行分类讨论。
- 若两个点的最近公共祖先是一个圆点,那么上述求法是完全没有问题的
- 若两个点的最近公共祖先是一个方点,说明这两个点的最近公共最先在一个环上,这个就有点棘手了
对于第二个情况,我放个图可能更形象。
你会发现此时 \(a,b\) 的最短距离应该为:蓝色虚线+粉红色或者蓝色虚线+棕色,那么这两种情况取最小值就是答案。
不过问题又来了,如何快速查询每个环上的两点间的距离呢,我们可以用前缀和来实现!特殊地,把每个环的“头”当作前缀和的第一个数,然后顺时针或者逆时针遍历环即可。
最后就是一堆细节了 hh
Code
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12010, M = N * 3;
int n, m, Q, new_n;
int h1[N], h2[N], e[M], w[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int s[N], stot[N], fu[N], fw[N], fe[M];
int fa[N][14], depth[N], dist[N];
int A, B;
void add(int h[], int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++ ;
return;
}
void build_circle(int x, int y, int z)
{
int sum = z;
for (int k = y; k != x; k = fu[k])
{
s[k] = sum;
sum += fw[k];
}
s[x] = stot[x] = sum;
add(h2, x, ++ new_n, 0);
for (int k = y; k != x; k = fu[k])
{
stot[k] = sum;
add(h2, new_n, k, min(s[k], sum - s[k]));
}
return;
}
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
for (int i = h1[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
fu[j] = u;
fw[j] = w[i];
fe[j] = i; //fe[j]存储j由那条边下来,这样可以处理重边问题
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j]) add(h2, u, j, w[i]);
}
else if (i != (from ^ 1)) low[u] = min(low[u], dfn[j]);
}
for (int i = h1[u]; ~i; i = ne[i])
{
int j = e[i];
if (dfn[u] < dfn[j] && fe[j] != i)
build_circle(u, j, w[i]);
}
return;
}
void dfs_lca(int u, int father)
{
depth[u] = depth[father] + 1;
fa[u][0] = father;
for (int k = 1; k <= 13; k ++ )
fa[u][k] = fa[fa[u][k - 1]][k - 1];
for (int i = h2[u]; ~i; i = ne[i])
{
int j = e[i];
dist[j] = dist[u] + w[i];
dfs_lca(j, u);
}
return;
}
int lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
for (int k = 13; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
for (int k = 13; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
A = a, B = b;
return fa[a][0];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> Q;
new_n = n;
memset(h1, -1, sizeof h1);
memset(h2, -1, sizeof h2);
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(h1, a, b, c);
add(h1, b, a, c);
}
tarjan(1, -1);
dfs_lca(1, 0);
while (Q -- )
{
int a, b;
cin >> a >> b;
int p = lca(a, b);
if (p <= n) cout << dist[a] + dist[b] - 2 * dist[p] << endl;
else
{
int da = dist[a] - dist[A];
int db = dist[b] - dist[B];
int l = abs(s[A] - s[B]);
int r = stot[A] - l;
int dm = min(l, r);
cout << da + dm + db << endl;
}
}
return 0;
}
后语
挺麻烦的,但是理解了的话就好背了,敲代码多是一件美逝!
完结撒花~