洛谷 P5236 【模板】静态仙人掌 题解

P5236 【模板】静态仙人掌

前言

我认为仙人掌是比较综合的题,涉及到了边双连通分量、最近公共祖先(甚至有些题目会用到树链剖分、动态树等等)。

所以呢,请先把下面的题掌握了再看此题

圆方树

因为仙人掌是有环的,这就导致求最短路的时候及其不方便,那么不妨将仙人掌做一个变形,变成一棵树,然后再进行操作,似乎就可做了。

这里引进“圆方树”,正如它的名字,圆方树中有“圆点”和“方点”两种点。圆方树该怎么建?看下面的图

你会发现一些有趣的性质:

  • 圆方树的圆点 = 仙人掌的总点数
  • 圆方树的方点 = 仙人掌的总环数

那么,圆方树具体是怎么建立的呢?

  1. 选取任意一个点为圆方树的跟。
  2. 如果该点在某一个环上,那么建立一个新的方点,并使该点向方点连一条权值为 0 的有向边,并把该点叫做这个环的“头”,再将环上其他点变成圆点,从方点依次向它们连一条权值为 k 的有向边。(k是什么详见下文)
  3. 如果该点不在某一个环上,那么建立若干个新的圆点,并使该点向新圆点连一条权值为 k 的有向边,这些新的圆点的数量是还没有遍历过的且与该点相邻的点的数量(其实说白了就是形态不变,从仙人掌那里复制过来建到圆方树里)
  4. 将这些新建的点再执行第 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;
}

后语

挺麻烦的,但是理解了的话就好背了,敲代码多是一件美逝
完结撒花~

posted @ 2022-08-08 08:33  LittleMoMol  阅读(77)  评论(0编辑  收藏  举报
*/