思路:
首先是要建最大生成树,不难发现,对于两点\(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;
}