圆方树

前言

基础算法: Tarjan,双连通分量,树链剖分。

导入

给你一个 n n n 个节点 m m m 条边的仙人掌图,让你在线查询 u u u v v v 的最短路径长度。
其中 1 ≤ n ≤ 1000 1 \le n \le 1000 1n1000
仙人掌图:任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。

首先看到这道题,我们会自然想到Floyd以 O ( n 3 ) O(n^3) O(n3) 求解这道题,但数据范围 1 ≤ n ≤ 1000 1\le n \le 1000 1n1000 显然会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]2dis[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;
}

广义圆方树

前面的圆方树只能解决仙人掌问题,广义圆方树可以解决一般图上的圆方树。
如图:
在这里插入图片描述
这时我们会把割边所连接的两个节点也连接到一个方节点上去,于是我们形成的圆方树就有这样一个性质:圆节点与方节点是相间的,具体实现也差不多,在求点双时就把栈中的节点连接放节点即可。

完结撒花

圆方树的难点就在于对方节点的处理,我们需要应题目而定方节点的权值或所维护的东西,变成一棵圆方树后就和普通树差不多了。
圆方树就是码农加思维题目,其他的技巧运用得并不多,主要还是不好处理方节点,想清楚方节点的处理,题目就变简单了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值