Kruskal重构树 学习笔记

发布时间 2023-10-12 20:36:07作者: bhbjzyh

前言

也许在看这篇文章之前,你可以看看这篇文章

前置知识:\(kruskal\) 求最小生成树,并查集……

算法介绍

问题引入

两个点之间的所有简单路径上最大边权的最小值。

我们定义 \(u\to v\) 路径的瓶颈为,路径上的边权最大值。

那么下图的瓶颈就为 4:
image

同时一条路径也可能有多个瓶颈,求最小瓶颈。

实现方法

在做上面那道例题我们对于边权大的可以不考虑他的贡献,因为我们要的是路径上的最大值越小越好,那么我们可以在原图联通的条件下,尽量去边权小的。

我们想到了在最小生成树的时候学习到了 \(Kruskal\) 算法(就当你看过上面的文章了),我们是按照边权的大小从大到小进行排序,并将两端不联通的两个集合合并在一起。

于是就产生了 \(Kruskal\) 重构树,当我们连接两个点 \(u,v\) 时,我们先找到他们的祖先 \(fau,fav\),考虑创建一个新节点 \(nod\),使得 \(fau,fav\) 的父亲为 \(nod\),关于 \(nod\) 的点权 \(W_{nod}\) 我们通常设 \(W_{nod}\) 设为 \(u\to v\) 这条边的边权(如果是给的是点的限制条件,我们可以设这个点的权值为 \(\max(w_u,w_v)\mid\min(w_u,w_v)\) 这样可以使经过这条边的需要限制条件满足这个边权的值才可以走)。这在我们做题的时候可能会有意想不到的帮助。

对于构建步骤:

  • 新建n个集合,每个集合图中为一个节点,点权为0。

  • 按照边权从小到大顺序遍历边。

  • 判断当前这条边的两个顶点 \(u,v\) 是否在同一集合中,若不是,则新建一个节点 \(nod\),点权为当前边的权值,并且将两个集合的父亲设为 $nod $,然后将两个集合合并,设其根节点为新添加的这个点。

算了,还是模拟一下过程(加粗的为原节点)。

:我没加点权。

我们假设原图长这样:

我们将边从小到大进行排序,得到:

排名 1 2 3 4 5 6 7 8 9
\(u\) 2 2 1 2 3 1 1 4 4
\(v\) 5 3 6 4 4 5 4 6 5
\(w\) 1 2 3 3 4 5 6 7 8

然后开始重构。

\((2,5)\) 这条边加入,先找到 \(2,5\) 的祖先 \(2,5\),新建节点 \(7\),将 \(2\to 7,5\to 7\),现在的图应该是:

然后再加入 \((2,3)\) 这条边,先找到 \(2,3\) 的祖先 \(7,3\),新建节点 \(8\),将 \(7\to 8,3\to 8\)

再加入 \((1,6)\) 这条边,找到 \(1,6\) 的祖先 \(1,6\),新建节点 \(9\),将 \(1\to 9,6\to 9\)

再加入 \((2,4)\) 这条边,找到 \(2,4\) 的祖先 \(8,4\),新建节点 \(10\),将 \(4\to 10,8\to 10\)

加入 \((1,5)\) 这条边,找到 \(2,4\) 的祖先 \(10,10\),发现他们是一个子树内的,不需要加进去。

加入 \((1,5)\) 这条边,找到 \(1,5\) 的祖先 \(9,10\),新建节点 \(11\),将 \(9\to 11,10\to 11\)

$Kruskal$ 重构树模版
struct XXX{
    int u,v,w;
}a[600005];
inline bool cmp(XXX x,XXX y) {
	return x.w<y.w;
}

int find(int fa){
	if(f[fa]==fa) return fa;
	return f[fa]=find(f[fa]);
}
int merge(int u,int v){
	int fau=find(u),fav=find(v);
	f[fav]=fau;
}
int tot;
vector<int>q[600007];
inline void addEdge(int u, int v){
    q[u].push_back(v);
    q[v].push_back(u);
}
int W[600007];
inline void Kruskal() {
	for(int i=1;i<=2*n+m;i++) f[i]=i;
	sort(a+1,a+m+1,cmp);
	for (register int i=1;i<=m;i++){
		XXX e=a[i];
		int u=e.u,v=e.v,w=e.w;
		if(find(u)!=find(v)){
			int fau=find(u),fav=find(v);
			int nod=++tot;
			W[nod]=w;
			f[nod]=nod;
			addEdge(nod,fau);
			addEdge(nod,fav);
            merge(tot,fau);
			merge(tot, fav);
		}
        if (tot==n*2-1)
            break;
	}
}

性质

  • 重构树是一个二叉树,且如果原图有 \(n\) 个点,那么重构树有 \(2*n-1\) 个点,树上的每一个叶节点都是原图上的点。

  • 原生成树中两个节点之间简单路径的最大边权的最小值即为重构树中这两个点的最近公共祖先。

  • 在对于只经过全值不大于某个点或边的权值,我们只需要找到他祖先中,权值小于等于限制条件且深度最浅的一个点,它的所有子节点及时可以走到的所有地方。

例题

原题:P4768 [NOI2018] 归程

我们发现 \(v\)\(u\) 的路径一定在 \(u\)\(v\) 的最大生成树上。

考虑把边按照海拔进行从大到小排序,并求出该图的 \(Kruskal\) 重构树,然后对于 \(x\) 的祖先,只要有 \(W_x\le p\) 则我们一定可以走到 \(x\)\(Kruskal\) 重构树的性质)。

我们乘车的目的就是选择一个到一号点最短路最小的点下车,那么可以先跑一遍 \(dijkstra\) 算出一号节点与其他点的最短路径。

然后我们可以考虑倍增(一般的图论都需要倍增),利用倍增直接找到与 \(1\) 距离最小且 \(W_x\le p\) 的祖先即可。

P4768 [NOI2018] 归程
#include<bits/stdc++.h>
using namespace std;
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(!isdigit(c)) {
		if(c=='-')
			f=-1;
		c=getchar();
	}
	while(isdigit(c))
		x=x*10+c-'0',c=getchar();
	return x*f;
}
int n,m;
int Tot;
struct Edge {
    int to, v, w;
}grape[800005];
vector<Edge>G[800005];
inline void AddEdge(int u, int v, int z, int w)
{
    G[u].push_back((Edge){v,z,w });
    G[v].push_back((Edge){ u,z,w });
}
inline bool cmp(Edge x,Edge y) {
	return x.w>y.w;
}
struct DSU {
	int fa[800005];
	inline void dsU(){
		for(register int i=1;i<=n+m;i++)fa[i]=i;
	}
	inline int find(int x){
		if (fa[x]==x)return x;
		return fa[x]=find(fa[x]);
	}
	inline bool same(int u,int v) {
		return find(u)==find(v);
	}
	inline void merge(int u,int v) {
		u=find(u),v=find(v);
		if(u==v) return;
		fa[v]=u;

	}
} dsu;
struct node {
	int wei;
	int id;
	bool operator <(const node& x)const {
		return x.wei<wei;
	}
};
int d[800005];
inline void dij() {
	priority_queue<node> q;
	for(register int i=1;i<=n;i++)
		d[i]=INT_MAX;
	d[1]=0;
	q.push((node){0,1});
	while(!q.empty()){
		node tmp=q.top();
		q.pop();
		int I=tmp.id,W=tmp.wei;
		if(W>d[I])
			continue;
        for(auto& e:G[I]){
            if (d[I]+e.v<d[e.to]){
                d[e.to]=d[I]+e.v;
                q.push((node){d[e.to],e.to});
            }
        }
	}
}
vector<int>Q[800005];
int tot;
int W[800005];
inline void Kruskal() {
	sort(grape+1,grape+m+1,cmp);
	dsu.dsU();
	for (register int i=1;i<=m;i++){
		Edge e=grape[i];
		int u=e.to,v=e.v,w=e.w;
		if (!dsu.same(u, v)){
			int fau=dsu.find(u),fav=dsu.find(v);
			int nod=++tot;
			W[nod]=w;
			dsu.fa[nod]=nod;
			Q[nod].push_back(fau);
			Q[fau].push_back(nod);
			Q[nod].push_back(fav);
			Q[fav].push_back(nod);
            dsu.merge(tot, fau), dsu.merge(tot, fav);
		}
        if (tot==n*2-1)
            break;
	}
}
int f[800005],g[800005][29];
inline void dfs(int x,int p){
    g[x][0]=p;
    if(x<=n) f[x]=d[x];
    else f[x]=INT_MAX;
    for(auto v:Q[x]){
        if(v==p)
            continue;
        dfs(v,x);
        f[x]=min(f[v],f[x]);
    }
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 
	int T=read();
	while(T--) {
		memset(f,0,sizeof(f));
		memset(g,0,sizeof(g));
		memset(W,0,sizeof(W));
		n=read();
		m=read();
		for(register int i=1;i<=n+m;i++)
			Q[i].clear();
		for(register int i=1;i<=n;i++)
			G[i].clear();
		tot=n;
		for(register int i=1;i<=m;i++){
			grape[i].to=read();
			grape[i].v=read();
			int z=read();
			grape[i].w=read();
			AddEdge(grape[i].to,grape[i].v,z,grape[i].w);
			AddEdge(grape[i].v,grape[i].to,z,grape[i].w);
		}
		dij();
		Kruskal();
		dfs(tot,0);
        for(int i=1;(1<<i)<=tot;i++)
			for(register int u=1;u<=tot;u++)
        		g[u][i]=g[g[u][i-1]][i-1];
		int Ques=read();
		int k=read(),s=read();
		int lastans=0;
		while(Ques--){
			int x=read();
			x=(x+k*lastans-1)%n+1;
			int y=read();
			y=(y+k*lastans)%(s+1);
			for(int i=25;i>=0;--i)
				if(g[x][i]>0&&W[g[x][i]]>y)
					x=g[x][i];
			lastans=f[x];
			cout<<f[x]<<endl;
		}
	}
	return 0;
}