货车运输

思路:

首先是要建最大生成树,不难发现,对于两点\(u,v\),如果\(u->v\)中最小的边的权值最大,那么这条路径\(u->v\)一定在最大生成树上。

考虑用\(Kruskal\)建最大生成树,那么首先对边按照从大到小进行排序。

然后用并查集维护两个点的联通性,如果\(u\)\(v\)不在同一个并查集中就合并,注意到合并的同时,我们保证了这样的一个信息:

在对第\(i\)条边操作的时候,假设合并了并查集\(S1\)与并查集\(S2\),那么显然有:(记第\(i\)条边的边权为\(V_i\)


性质1.集合\(S1\)内点,集合\(S2\)内点分别可以相互到达 (由前面\(Kruskal\)建最大生成树的流程得到的)

性质2.集合\(S1,S2\)内,任意两点之间的边权的权值都比\(Ki\)大(因为已经按照边权排序了)


那么我们不难发现,从集合\(S1\)内的点出发,到集合\(S2\)内的点,所经过的路径中,边权最小的即为\(V_i\),换而言之,我们现在的目标是维护这个东西。也就是说,对于某一次询问\(u\)\(v\),我能告诉你\(u\)\(v\)两个点被合并到一个集合时,通过的边的边权。(这一段感性理解,因为打字不好描述)

如何维护?/流程,做法

为了方便起见,我们认为\(V_i\)表示第\(i\)条边的边权。(貌似这个做法又被叫做\(Kruskal\)重构树?)

仍然按照\(Kruskal\)建最大生成树的方法,对边按照边权排序。

//下面是我自己可能也看不懂的解释,但还是强行解释了一波流程...\(QAQ\)

(您可以忽略这一段,直接跳到下面清晰好看的字体附近)


第一步,首先考虑连接两个点的边,假设边\(i\)连接了点\(u\)\(v\),那么我可以建立一个节点,这个点的点权为\(V_i\),然后其拥有两个儿子,分别指向\(u\)\(v\),那么我询问\(u\)\(v\)的路径长度,就是\(u\)\(v\)两点的父亲。然后把\(u\)\(v\)合并到一个并查集中,表示\(u\)\(v\)联通。然后记这个并查集为\(S1\),其最顶上的根节点记为\(root1\),这样做,我们就建出了一个二叉树。

然后考虑假设边\(i\)连接了两个大小为\(2\)的并查集\(S1\)\(S2\),这个大小为\(2\)的并查集即第一步建立的并查集,那么我建立一个节点\(k\),其点权为\(V_i\),那么其两个儿子则分别指向并查集\(S1,S2\)\(root1,root2.\)不难发现,从并查集\(S1\)中的点到并查集\(S2\)中的点,必须要经过点\(k\)。那么集合\(S1\)中的点到集合\(S2\)中的点所经过的点,就是在以此法建出来的树中,经过的点的点权最小值,也就是\(k\)的点权。

依次类推,对于一条边\(i\),如果其连接了并查集\(S1\)\(S2\),仍然有之前得到的性质\(1\)和性质\(2\),所以建立一个节点\(k\),令其二个儿子分别指向\(S1\)\(S2\)\(root\),然后令其点权为\(V_i\)


简化建树流程如下:



> 1.按照边权排序

> 2.对于一条边所指向的两个点,判断其是否在同一集合,若不在,合并两个集合,把这条边当作一个树点,新建节点,权值为这条边的边权,令其两个儿子指向这两个集合的树的祖先root。

> 3.不难发现,对于u->v的路径,其在最大生成树中唯一,且在重构树中也唯一,即从叶子节点u到叶子节点v的路径。

> 4.同时,由于建树流程只是在Kruskal建树流程加了一步,所以我仍然有前面所提到的性质1和性质2,所以我们有对于一个父亲节点,若两个儿子为边,则儿子权值一定比父亲大(性质2)。所以询问集合S1到集合S2的最小限重,就是这个点的点权。

> 5.对于第4点反过来考虑,询问u->v的答案,实际上就是询问重构树中,点u和点v的LCA的权值

即对于询问\(u->v\)最小限重,我们求出其\(LCA\),然后返回\(LCA\)的权值即可。

所以我们的问题变成,在重构的树上求\(LCA\)了。。。

所以用\(Trajan\)算法呗......

复杂度貌似就排序时会有\(log\),总复杂度\(O(MlogM)\)

代码://笔者写得比较丑......因为弱.......

#include<bits/stdc++.h>
using namespace std;
int read(){
	char cc = getchar(); int cn = 0;
	while(cc > '9' || cc < '0') cc = getchar();
	while(cc >= '0' && cc <= '9') cn = cn * 10 + cc - '0', cc = getchar();
	return cn;
}
const int M = 50000 * 2 + 5;
struct E{
	int to, w, next, from;
	bool operator < (const E &a) const{
		return w > a.w;
	}// 重定义< 号 
}e[M * 2];
struct Tree{
	int son[2], fa, val, p, root; //p表示这个点为边还是点 
}t[M * 2];
int head[M], cnt, book[M], n, m, fa[M], tot, ans[M], top[M];
int Head[M], To[M], Next[M], Id[M], tot1;
//加边 
void add(int x, int y, int z)
{
	e[++cnt].to = y;  e[cnt].from = x;  e[cnt].w = z;
	e[cnt].next = head[x];  head[x] = cnt;
} 
//---并查集 
//查找 
int find(int x){
	if(x == fa[x]) return x;
	return fa[x] = find(fa[x]);
}
//合并 
void merge(int x, int y){
	int u = find(x), v = find(y);
	fa[u] = v;
}
//---重构树--- 
int find_top(int x){ //询问root,即当前点x的祖先 
	while(t[x].fa != 0) x = t[x].fa;
	return x;
}
void merge1(int x, int y, int w){//新建节点,同时将两棵树变为此点儿子 
	t[++tot].son[0] = x;  t[tot].son[1] = y;
	t[tot].val = w;
	t[x].fa = tot; t[y].fa = tot;
	t[x].root = 0, t[y].root = 0, t[tot].root = 1;
}
void Kruskal(){//重构树 
	for(int i = 1; i <= n; i++){
		fa[i] = i;//并查集初始化 
		t[++tot].p = 1; t[tot].val = i; t[tot].root = 1, top[i] = i;//每个节点对应的树初始化 
	}
	sort(e + 1, e + cnt + 1);//排序 
	for(int i = 1; i <= cnt; i++){
		int u = find(e[i].from), v = find(e[i].to);
		if(u != v){//不在同一个并查集 
			merge(u, v);
			int u1 = find_top(top[e[i].from]); //top记录了i号点,在当前情况下,祖先是谁,在询问i号的时候更新其top,防止跳跃次数比较多,算是一个小优化 
			int v1 = find_top(top[e[i].to]);
			merge1(u1, v1, e[i].w);
			top[e[i].to] = tot; top[e[i].from] = tot;
		}
	}
}
//---询问---Trajan 
void add_q(int x, int y, int z){
	To[++tot1] = y;	Id[tot1] = z;
	Next[tot1] = Head[x]; Head[x] = tot1;
}
void dfs(int now){
	book[now] = 1;
	if(t[now].p){
		for(int i = Head[t[now].val]; i; i = Next[i])
			if(book[To[i]])  ans[Id[i]] = t[find(To[i])].val;
		return ;
	}
	dfs(t[now].son[0]);  fa[t[now].son[0]] = now;
	dfs(t[now].son[1]);  fa[t[now].son[1]] = now;
}
void input(){
	n = read(); m = read();
	int x, y, z;
	for(int i = 1; i <= m; i++)
	{
		x = read(); y = read(); z = read();
		add(x, y, z);
		add(y, x, z); 
	}
}
void solve(){
	int p, x, y; 
	p = read();
	for(int i = 1; i <= p; i++)
	{
		x = read(); y = read();
		add_q(x, y, i); add_q(y, x, i);
	}
	for(int i = 1; i <= tot; i++) fa[i] = i;
	t[0].val = -1; //因为可能图并不联通,所以会建出多棵重构树,然后令这些重构树的父亲都为0号点。那么其实通过0号点到达点,本质上是并不连通的,LCA为0,其LCA的权值为-1 
	for(int i = 1; i <= tot; i++){
		if(t[i].root == 1)//这个点为一个根节点 
			dfs(i);//dfs,trajan 
			fa[i] = 0; //然后把这个点父亲改为0 
	}
	for(int i = 1; i <= p; i++) printf("%d\n", ans[i]);
}
signed main()
{
	input();
	Kruskal();
	solve(); 
	return 0;
} 
原文地址:https://www.cnblogs.com/Soulist/p/10332010.html