Loading

学习笔记——Kruskal 重构树

引入

大家都知道 \(Kruskal\) 求最小生成树吧,这个算法就是建立在前面说的算法的基础上的一个奇妙的想法。
有这么一个问题,给你一张图,每条边都有权值,然后规定一堆东西后问:满足条件的路径中,所经过边权最大值最小是多少。

初步想法

二分,一定是最先想到的,我们接下来就以标题中的那题为例讲讲这个算法。

我们很容易想到二分最大边权,然后验证。但是每次验证都跑 \(dfs\) 肯定得炸。
那么怎么办呢。

来看看重构树算法的实现吧。

实现

首先,根据 \(Kruskal\) 的贪心思路,如果我从 \(x\) 节点出发,有较小的边可以走,肯定是不会走较大的边的,换句话说,就是一定是在图的最小生成树上走最优。

所以无疑,算法第一步:将边按权排序,求出最小生成树。

然后在求的过程中,就是本算法的构造了。对于我当前枚举的边所连接的两个点(或者点集),用一个虚拟节点建在上方作为这两个点的父节点,然后将这个父节点的权值赋为边的权值。

也许不是很清楚,那么来看看题目中样例这个图:
(注意本题是把编号当成边权的)
在这里插入图片描述
\(Kruskal\) 一样,先取出 \(Edge(2,3)\),然后建立一个父子关系,加入到同一集合里。已经有 \(5\) 个点了,所以他们的父节点就记为 \(6\),权值为 \(1\)
在这里插入图片描述
然后是 \(Edge(4,5)\),进行同样的操作。
在这里插入图片描述
然后是 \(Edge(2,1)\),但是此时 \(2\) 已经属于一个点集了,所以实质上是把 \(1\)\(2\) 所在的点集连起来,所以应该是连结 \((1,6)\)
在这里插入图片描述
然后查到 \(Edge(1,3)\),但是在前面的并查集中可以查到,\(1\)\(3\) 已经在同一集合里了,所以跳过。
所以最后一条边是 \(Edge(1,4)\),实质是把 \(8,7\) 连起来。
其他的都会跳过,因为已经合并好了。
在这里插入图片描述
然后就可以在这棵树上做一些奇奇怪怪的操作了,因为它有一个美妙的性质:每一条树枝上,边权是单调的,那么可以倍增来快速找到最大的不超过某个值的最小位置是哪里了。

比如本题,就可以二分答案,然后对于 \(x\)\(y\),分别向上跳到点权是大于我二分的值为止,然后向下子树中叶节点个数一加就是我经过点的个数,和 \(z\) 比一下,就可以实现这个二分了。
\(\color{Red}{Ps:}\) 一定一定一定要注意倍增不要写错以及,如果 \(xy\) 跳到一起去了,只能算一个。

相信理解起来不难吧。上代码。

Code

#include<bits/stdc++.h>
#define ll long long
#define inf 1<<30
using namespace std;
const int MAXN=2e5+10;
vector<int> vec[MAXN];
void add(int fa,int u,int v){
	vec[fa].push_back(u);
	vec[fa].push_back(v);
}
int cnt,son[MAXN],f[MAXN][20],v[MAXN];
void dfs(int x,int fa){
	f[x][0]=fa;
	for(int i=1;i<20;i++) f[x][i]=f[f[x][i-1]][i-1];
	if(vec[x].size()==0){son[x]=1;return;}
	son[x]=0;
	for(int i=0;i<vec[x].size();i++){
		int s=vec[x][i];
		if(s==fa) continue;
		dfs(s,x);son[x]+=son[s];
	}
}//预处理倍增和子树中叶节点的个数
int check(int p,int x,int y){
	for(int i=19;i>=0;i--){
		if(v[f[x][i]]<=p) x=f[x][i];
		if(v[f[y][i]]<=p) y=f[y][i];
	}
	if(x==y) return son[x];
	else return son[x]+son[y];
}//验算,得到经过节点个数
int ff[MAXN];
int find(int x){return ff[x]==x?x:ff[x]=find(ff[x]);}
int main()
{
	int n,m,x,y,z;
	scanf("%d%d",&n,&m);cnt=n;
	for(int i=1;i<=2*n;i++) v[i]=0,ff[i]=i;
	v[0]=inf;//防止跳到根的外面去
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		int fx=find(x),fy=find(y);
		if(cnt<n*2-1&&fx!=fy){
			cnt++;
			v[cnt]=i;
			ff[fy]=cnt;
			ff[fx]=cnt;//这里其实和裸的Kruskal很像的,但是由于有新建的节点所以更优美了
			add(cnt,fx,fy);
		}//本题的特殊性,否则需要按权排序后再做
	}
	dfs(cnt,0);int Q;
	for(scanf("%d",&Q);Q--;){
		scanf("%d%d%d",&x,&y,&z);
		int ans=m,l=0,r=m;
		while(l<=r){
			int mid=l+r>>1;
			if(check(mid,x,y)<z) l=mid+1,ans=l;
			else r=mid-1;
		}
		printf("%d\n",ans);
	}
}

END

很多人用可持久化并查集和整体二分过了这题……

posted @ 2021-01-03 16:10  ZCETHAN  阅读(455)  评论(0编辑  收藏  举报