Kruskal重构树(AT1998 Stamp Rally)

引入

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

初步想法

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

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

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

实现

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

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

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

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值