前言
基础算法: Tarjan,双连通分量,树链剖分。
导入
给你一个
n
n
n 个节点
m
m
m 条边的仙人掌图,让你在线查询
u
u
u 到
v
v
v 的最短路径长度。
其中
1
≤
n
≤
1000
1 \le n \le 1000
1≤n≤1000
仙人掌图:任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。
首先看到这道题,我们会自然想到Floyd以 O ( n 3 ) O(n^3) O(n3) 求解这道题,但数据范围 1 ≤ n ≤ 1000 1\le n \le 1000 1≤n≤1000 显然会TLE。又因为这个图是仙人掌图,每条边都只出现在一个环中,于是我们可以用圆方树求解。
算法
仙人掌上的圆方树
在执行Tarjan求双连通分量时,我们可以得到每一个点分别属于哪些环中,于是我们就可以对每个这个环上的点连接一个新节点,于是就把原图变成了一棵树。
如图:
性质
这是用原图建立一棵圆方树的过程,其中圆节点是原图上的节点,方节点是对于每个环连接的节点。
显然有几个性质成立:
1.每个方节点一定是连接的圆节点,方节点与方节点不会相连。
2.显然建出的是一堆无根树构成的森林,原图上联通的点圆方树(森林)上也联通
3.所有度数
>
1
> 1
>1 的圆点在原图中都是割点
这样我们就建立了一棵圆方树,圆方树是一种十分优秀的结构,我们可以在圆方树上进行各种操作,树形DP、树剖…
实现步骤
对于每一个节点,在Tarjan中找到返祖边,也就是现在形成了一个节点数大于一的一个环,就把环上的所有节点连向一个方节点,因为这些节点在搜索树中是连续的,我们可以将它们的父亲节点标出来,由返祖边所连接的两个节点组成的子图就是这个环。对于其他割边,我们选择保留原图数据,直接连边。
因为是一个仙人掌图,考虑到要转移成的树符合最短路径,又因为方节点连接的是一个环,所以我们可以在这个环中找到到某一个节点的最短路径作为边权,具体的边权需以题目要求而定。
对于上述的题目,查询时我们需分两种情况而定:
1.若
l
c
a
lca
lca 是圆点,那么答案就是
d
i
s
[
u
]
+
d
i
s
[
v
]
−
2
∗
d
i
s
[
l
c
a
]
dis[u] +dis[v] - 2 * dis[lca]
dis[u]+dis[v]−2∗dis[lca]
2.若
l
c
a
lca
lca 是方点,则找到进入这个环的两个点,这两个点之间的有两条路径,比较一下选较短的,也就是找到是方节点的两个儿子并且是
u
u
u 与
v
v
v 的祖先,找到分别的距离相加即可。
具体代码
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <map>
using namespace std;
const int MAXN = 8e4 + 5;
int n,m,q,w[MAXN],head[MAXN],tot,rect,dis[MAXN];
int dfn[MAXN],low[MAXN],b[MAXN],top,Time,sc,scc[MAXN],Size[MAXN],Fa[MAXN],ToT;
int Head[MAXN],Tcnt,sum[MAXN],dist[MAXN];
int Dfn[MAXN],Son[MAXN],TSize[MAXN],father[MAXN],dep[MAXN],TIME;
int Tree_Top[MAXN];
struct node{
int To,Next,Val;
}edge[MAXN],Edge[MAXN];
struct Segment_Tree{
int l,r,Minn;
}Tree[MAXN];
void Add(int x,int y,int z) {
edge[++tot].To = y;
edge[tot].Next = head[x];
edge[tot].Val = z;
head[x] = tot;
}
void Add_Edge(int x,int y,int z) {
Edge[++ToT].To = y;
Edge[ToT].Next = Head[x];
Edge[ToT].Val = z;
Head[x] = ToT;
}
int read () {
int f = 1, num = 0;
char s = getchar ();
while (s > '9' || s < '0') {
if (s == '-') f = -1;
s = getchar ();
}
while (s >= '0' && s <= '9') {
num = (num << 3) + (num << 1) + (s - '0');
s = getchar ();
}
return num *= f;
}
void write (int x) {
if (x < 0) {
putchar ('-');
x = (~x) + 1;
}
if (x > 9) {
write (x / 10);
}
putchar (x % 10 + '0');
}
void Solve_Graph(int u,int v,int w) {
++ rect;
int pre = w,now = v;//访问环上的节点
while(now != Fa[u]) {
sum[now] = pre;
pre += b[now];
now = Fa[now];
}
sum[rect] = sum[u];//方节点的权值就是环上的权值之和
sum[u] = 0;
now = v;
while(now != Fa[u]) {
int mst = min(sum[now],sum[rect] - sum[now]);//找到对于 u 的最短路径
Add_Edge(now,rect,mst);
Add_Edge(rect,now,mst);
now = Fa[now];
}
}
void Tarjan(int x,int fa) {
dfn[x] = low[x] = ++Time;
for(int i = head[x];i;i = edge[i].Next) {
int To = edge[i].To;
if(To == fa) continue;
if(!dfn[To]) {
b[To] = edge[i].Val;//环上的边权下放到点上
Fa[To] = x;//父亲节点
Tarjan(To,x);
low[x] = min(low[x],low[To]);
} else low[x] = min(low[x],dfn[To]);
if(low[To] > dfn[x]) {//圆点之间连边
Add_Edge(x,To,edge[i].Val);
Add_Edge(To,x,edge[i].Val);
}
}
for(int i = head[x];i;i = edge[i].Next) {
int To = edge[i].To;
if(Fa[To] == x || dfn[To] <= dfn[x]) continue;
Solve_Graph(x,To,edge[i].Val);//找到了一个环
}
}
void dfs1(int x,int fa) {
father[x] = fa,dep[x] = dep[fa] + 1;
TSize[x] = 1;
int Son_Size = -1;
for(int i = Head[x];i;i = Edge[i].Next) {
int To = Edge[i].To;
if(To == fa) continue;
dis[To] = dis[x] + Edge[i].Val;
dfs1(To,x);
TSize[x] += TSize[To];
if(TSize[To] > Son_Size) {
Son_Size = TSize[To];
Son[x] = To;
}
}
}
void dfs2(int x,int Top) {
Tree_Top[x] = Top;
if(!Son[x]) return;
dfs2(Son[x],Top);
for(int i = Head[x];i;i = Edge[i].Next) {
int To = Edge[i].To;
if(To == father[x] || To == Son[x]) continue;
dfs2(To,To);
}
}
int Query(int l,int r) {
while(Tree_Top[l] != Tree_Top[r]) {
if(dep[Tree_Top[l]] < dep[Tree_Top[r]]) swap(l,r);
l = father[Tree_Top[l]];
}
if(dep[l] > dep[r]) return r;
return l;
}
int Find_(int x,int y) {
int temp = x;
while(Tree_Top[x] != Tree_Top[y]) {
temp = Tree_Top[x];
x = father[Tree_Top[x]];
}
return (x == y) ? temp : Son[y];
}
int main() {
n = read(),m = read(),q = read();
rect = n;
for(int i = 1;i <= m;i ++) {
int u,v,w;
u = read();
v = read();
w = read();
Add(u,v,w),Add(v,u,w);
}
Tarjan(1,0);
dfs1(1,0);//执行树链剖分
dfs2(1,1);
for(int i = 1;i <= q;i ++) {
int u,v;
u = read();
v = read();
int Lca = Query(u,v);
if(Lca <= n) write(dis[u] + dis[v] - 2 * dis[Lca]);//lca是圆节点
else {//lca是方节点
int cy = Find_(u,Lca),qy = Find_(v,Lca);//对应的两个方节点儿子
int ex_wt = min(abs(sum[cy] - sum[qy]),(sum[Lca] - abs(sum[cy] - sum[qy])));//两个儿子之间的最短距离
// u->v 的距离等于 u->cy + v->qy + cy->qy
write(dis[u] + dis[v] - dis[cy] - dis[qy] + ex_wt);
}
putchar('\n');
}
return 0;
}
广义圆方树
前面的圆方树只能解决仙人掌问题,广义圆方树可以解决一般图上的圆方树。
如图:
这时我们会把割边所连接的两个节点也连接到一个方节点上去,于是我们形成的圆方树就有这样一个性质:圆节点与方节点是相间的,具体实现也差不多,在求点双时就把栈中的节点连接放节点即可。
完结撒花
圆方树的难点就在于对方节点的处理,我们需要应题目而定方节点的权值或所维护的东西,变成一棵圆方树后就和普通树差不多了。
圆方树就是码农加思维题目,其他的技巧运用得并不多,主要还是不好处理方节点,想清楚方节点的处理,题目就变简单了。