克鲁斯卡尔重构树学习笔记 & luogu P4197 Peaks

背景:

下午翘课,因为 要拍视频,懒得去。

题目传送门:

https://www.luogu.org/problem/P4197

题意:

n n n个点, m m m条边,有点权,有边权。
多组询问,求只经过权值小于等于 x x x的边的第 k k k大的点的权值。

思路:

容易想到离线按照询问点权升序,本质就是一棵树(由于已经连通,多余的边是废的),显然有一个主席树+主席树合并的做法,但是考虑到主席树合并我从来没有打过,而且树上的主席树实现应该比较繁琐,我还是放弃了。

发现正解是克鲁斯卡尔重构树。
于是学习了一发。
这是什么东西呢?
先讲一下它的构造过程吧:
类似克鲁斯卡尔最小生成树的方法,按照边权升序,若当前的点对 ( x , y ) (x,y) (x,y)并不连通,则新建一个点 T T T,让 T T T作为这两个点的祖先,且此时 T T T的边权为边 ( x , y ) (x,y) (x,y)的权值。
这样建出的树有什么性质吗?
[ 1 ] . [1]. [1].这一定是一个大根堆。
显然每一次都是两个点的合并,因此是二叉树;而越往后的边边权越大,因此越往后新建的点点权越大,而新的点必然统治旧的点,因此是一个大根堆。
[ 2 ] . [2]. [2].任意两条路径上的点权的最大值为它们 lca \text{lca} lca的值。
这也比较显然,因为这两个点第一次连通必然通过 lca \text{lca} lca,而这是一个大根堆,因此得证。

那么你用一个并查集维护,然后实现建树的过程即可。
剩下的就是倍增找到某一个点的最远的祖先,使得他的权值小于等于 x x x,然后在这个点的子树内查找第 k k k大的值即可,可用 dfs \text{dfs} dfs序+主席树实现。

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
	struct node1{int x,y,next;} a[500010];
	struct node2{int x,y,z;} b[500010];
	int c[200010],last[200010],fa[200010],val[200010],id[200010],f[200010][20],size[200010];
	int ls[5000010],rs[5000010],root[5000010],tot[5000010];
	int n,m,q,len=0,ma=0;
bool cmp(node2 x,node2 y)
{
	return x.z<y.z;
}
int find(int x)
{
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
void ins(int x,int y)
{
	a[++len]=(node1){x,y,last[x]}; last[x]=len;
}
void change(int last,int &now,int x,int l,int r)
{
	if(!now) now=++len;
	tot[now]=tot[last]+1;
	if(l==r) return;
	int mid=(l+r)/2;
	if(x<=mid)
	{
		rs[now]=rs[last];
		change(ls[last],ls[now],x,l,mid);
	}
	else
	{
		ls[now]=ls[last];
		change(rs[last],rs[now],x,mid+1,r);
	}
}
int solve(int x,int y,int k,int l,int r)
{
	if(tot[y]<k) return -1;
	if(l==r) return l;
	int tmp=tot[rs[y]]-tot[rs[x]],mid=(l+r)/2;
	return k<=tmp?solve(rs[x],rs[y],k,mid+1,r):solve(ls[x],ls[y],k-tmp,l,mid);
}
int u=0;
void dfs(int x)
{
	id[x]=++u;
	size[x]=1;
	if(x<=n) change(root[id[x]-1],root[id[x]],c[x],0,ma); else root[id[x]]=root[id[x]-1];
	for(int i=1;i<=19;i++)
		f[x][i]=f[f[x][i-1]][i-1];
	for(int i=last[x];i;i=a[i].next)
	{
		int y=a[i].y;
		f[y][0]=x;
		dfs(y);
		size[x]+=size[y];
	}
}
int find_(int x,int y)
{
	for(int i=19;i>=0;i--)
		if(f[x][i]&&val[f[x][i]]<=y) x=f[x][i];
	return x;
}
int main()
{
	int x,y,k;
	scanf("%d %d %d",&n,&m,&q);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&c[i]);
		ma=max(ma,c[i]);
		fa[i]=i;
	}
	for(int i=1;i<=m;i++)
		scanf("%d %d %d",&b[i].x,&b[i].y,&b[i].z);
	sort(b+1,b+m+1,cmp);
	int cnt=n;
	for(int i=1;i<=m;i++)
	{
		int t1=find(b[i].x),t2=find(b[i].y);
		if(t1==t2) continue;
		fa[++cnt]=cnt;
		fa[t1]=fa[t2]=cnt;
		val[cnt]=b[i].z;
		ins(cnt,t1),ins(cnt,t2);
		if(cnt==n+n-1) break;
	}
	len=0;
	dfs(cnt);
	for(int i=1;i<=q;i++)
	{
		scanf("%d %d %d",&x,&y,&k);
		int t=find_(x,y);
		printf("%d\n",solve(root[id[t]-1],root[id[t]+size[t]-1],k,0,ma));
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值