Kruskal 重构树相关
介绍
一种在省选题中较平凡的 \(\text{trick}\)。由建树和重构两部分组成,但最常见的写法是不建树而直接重构,这样能够降低代码复杂度。对于一类只关注连通性的题目,可以起到奇效。对于另一类既关注连通性,又关注最短路径等其他信息的题目,不那么套路,或者根本不可做。
概述
在 \(\text{Kruskal}\) 求出一条树边 \((u,v)\) 时,将 \(u,v\) 所在的并查集合并到新建的节点 \(x\) 上,在 \(\mathrm{find(u)}\) 和 \(x\) 间连一条边,在 \(\mathrm{find(v)}\) 与 \(x\) 间连一条边。并将新建节点 \(x\) 的点权赋为 \((u,v)\) 的权值。
原理
- 
基于 \(\mathrm{Kruskal}\) 求最小 \(/\) 大生成树的做法。
 - 
重构树的连通性与原树相同。
 
性质
- 
\(\mathrm{Kruskal}\) 重构树是一棵二叉树。(性质 \(1\))
 - 
\(\mathrm{Kruskal}\) 重构树上的点权满足堆的性质。 (性质 \(2\))
 - 
仅有 \(\mathrm{Kruskal}\) 重构树的叶子节点为原生成树上的节点,叶子节点没有点权。(性质 \(3\))
 - 
若将最小生成树重构,从原树上一点 \(x\) 出发仅经过权值不超过 \(k\) 的边能够到达的点 \(z\) 一定在 \(\mathrm{Kruskal}\) 重构树上 \(y\) 的子树内,其中 \(y\) 是 \(x\) 在重构树上向上只经过点权不超过 \(k\) 的点,能够到达的深度最小的祖先节点。最大生成树同理。(性质 \(4\))
 
对性质 \(2\) 的证明:按照边权单调加入生成树,加入越晚的一定深度越浅,因此对于任意 \(x\) 的父亲节点 \(fa\) 与节点 \(x\) 的点权一定存在单调性,即满足堆的性质。
对性质 \(4\) 的证明:性质 \(4\) 基于性质 \(2\)。并且重构树的连通性与原树一致,于是证毕(
为啥不证性质 \(1,3\) 呢?因为太显然了(
例题
题意:\(n\) 个点 \(m\) 条无向边的图,\(Q\) 组询问,询问从给定点 \(v_i\) 出发只经过边权 \(\leq x_i\) 的边能够到达的点中,具有第 \(k\) 大点权的点,要求强制在线。
根据原图的最小生成树建出 \(\mathrm{Kruskal}\) 重构树后,用倍增跳到重构树上深度最小的点权 \(\leq x_i\) 的点 \(y\),然后主席树维护 \(y\) 的子树内第 \(k\) 大即可。
注意重构树上点权为原生成树上边权,与题目给出点权不同。
#include<cstdio>
#include<algorithm>
int num=0,cnt=0,t=0;
struct edge {int x,y,w;} e[500005];
int c[200005],val[200005],b[200005];
int f[200005][25],g[200005][25];
int h[200005],to[400005],ver[400005];
int rt[200005],sonL[30000005],sonR[30000005],sum[30000005];
int fa[200005],bel[200005],dfn[200005],rev[200005],size[200005];
inline int read() {
	register int x=0,f=1;register char s=getchar();
	while(s>'9'||s<'0') {if(s=='-') f=-1;s=getchar();}
	while(s>='0'&&s<='9') {x=x*10+s-'0';s=getchar();}
	return x*f;
}
inline void add(int x,int y) {to[++cnt]=y;ver[cnt]=h[x];h[x]=cnt;}
inline int max(const int &x,const int &y) {return x>y? x:y;}
inline bool cmp(const edge &x,const edge &y) {return x.w<y.w;}
inline int find(int x) {return x==fa[x]? x:fa[x]=find(fa[x]);}
inline void assign(int x,int y) {sonL[x]=sonL[y];sonR[x]=sonR[y];sum[x]=sum[y];}
inline void prework(int x) {
	for(register int i=1;i<=20;++i) f[x][i]=f[f[x][i-1]][i-1],g[x][i]=max(g[x][i-1],g[f[x][i-1]][i-1]); size[x]=1; dfn[x]=++num; rev[num]=x;
	for(register int i=h[x];i;i=ver[i]) {int y=to[i]; f[y][0]=x; g[y][0]=val[x]; prework(y); size[x]+=size[y];}
}
inline int jump(int u,int lim) {for(register int i=20;i>=0;--i) {if(f[u][i]&&g[u][i]<=lim) u=f[u][i];} return u;}
inline void change(int &p,int lst,int l,int r,int x) {
	assign(p=++t,lst); ++sum[p]; if(l==r) return; int mid=l+r>>1;
	x<=mid? change(sonL[p],sonL[lst],l,mid,x):change(sonR[p],sonR[lst],mid+1,r,x);
}
inline int ask(int x,int y,int l,int r,int rk) {
	if(sum[y]-sum[x]<rk) return -1; if(l==r) return l; int mid=l+r>>1;
	if(sum[sonR[y]]-sum[sonR[x]]>=rk) return ask(sonR[x],sonR[y],mid+1,r,rk);
	return ask(sonL[x],sonL[y],l,mid,rk-(sum[sonR[y]]-sum[sonR[x]]));
}
int main() {
	int n=read(),m=read(),Q=read(),tot=n; int ans=0;
	for(register int i=1;i<=n;++i) b[++b[0]]=c[i]=read();
	for(register int i=1;i<=2*n-1;++i) fa[i]=i;
	std::sort(b+1,b+1+b[0]); b[0]=std::unique(b+1,b+1+b[0])-b-1;
	for(register int i=1;i<=m;++i) {e[i].x=read();e[i].y=read();e[i].w=read();} std::sort(e+1,e+1+m,cmp);
	for(register int i=1,fx,fy;i<=m;++i) if((fx=find(e[i].x))!=(fy=find(e[i].y))) {val[++tot]=e[i].w; fa[fx]=fa[fy]=tot; add(tot,fx); add(tot,fy);}
	for(register int i=n+1;i<=tot;++i) if(fa[i]==i) prework(i);
	for(register int i=1;i<=tot;++i) {int x=std::lower_bound(b+1,b+1+b[0],c[rev[i]])-b;if(c[rev[i]]) change(rt[i],rt[i-1],1,b[0],x); else rt[i]=rt[i-1];}
	while(Q--) {
		int v=read(),x=read(),k=read(); if(ans!=-1) v^=ans,x^=ans,k^=ans;
		int pos=jump(v,x); if(val[pos]>x||pos==v) {printf("-1\n"); ans=-1; continue;} 
		int res=ask(rt[dfn[pos]-1],rt[dfn[pos]+size[pos]-1],1,b[0],k);
		if(res!=-1) printf("%d\n",ans=b[res]); else printf("-1\n"),ans=-1;
	}
	return 0;
}
题意:\(n\) 个点 \(m\) 条无向边的图,每条边有两个信息 \((l,a)\),有 \(Q\) 个询问, 每次给出出发点 \(v_i\) 和约束 \(p_i\)。求从 \(v_i\) 出发到达 \(1\) 的最短路径,其中从 \(v_i\) 出发,\(a\geq p_i\) 的一段连续路径不计路径长度。
根据原图关于 \(a\) 的最大生成树建出 \(\mathrm{Kruskal}\) 重构树后,用倍增跳到重构树上深度最小的点权 \(\geq p_i\) 的点 \(y\),然后维护 \(y\) 的 子树内的点到达点 \(1\) 的最短路的最小值即可。最短路直接 \(\mathrm{dijkstra}\) 预处理。
#include<cstdio>
#include<queue> 
#include<functional>
#include<algorithm>
typedef long long ll;
const ll inf=1e15;
int cnt=0,n;
struct edge {int x,y,w1,w2;} e[400005];
ll dis[200005],minn[400005];
int f[400005][25],g[400005][25];
int fa[400005],val[400005],vis[200005];
int h[400005],to[800005],ver[800005],w[800005];
inline int read() {
	register int x=0,f=1;register char s=getchar();
	while(s>'9'||s<'0') {if(s=='-') f=-1;s=getchar();}
	while(s>='0'&&s<='9') {x=x*10+s-'0';s=getchar();}
	return x*f;
}
inline int min(const int &x,const int &y) {return x<y? x:y;}
inline ll min(const ll &x,const ll &y) {return x<y? x:y;} 
inline int max(const int &x,const int &y) {return x>y? x:y;}
inline void add(int x,int y,int z=0) {to[++cnt]=y;ver[cnt]=h[x];w[cnt]=z;h[x]=cnt;}
inline int find(int x) {return x==fa[x]? x:fa[x]=find(fa[x]);}
inline bool cmp(const edge &x,const edge &y) {return x.w2>y.w2;}
inline void dijkstra() {
	dis[1]=vis[1]=0; for(register int i=2;i<=n;++i) dis[i]=inf,vis[i]=0; 
	std::priority_queue<std::pair<ll,int> > Q; Q.push(std::make_pair(0,1));
	while(Q.size()) {
		int x=Q.top().second; Q.pop(); if(vis[x]) continue; vis[x]=1;
		for(register int i=h[x],y;i;i=ver[i]) if(dis[y=to[i]]>dis[x]+w[i]) {dis[y]=dis[x]+w[i];Q.push(std::make_pair(-dis[y],y));}
	}
}
inline void prework(int x) {
	for(register int i=1;i<=20;++i) f[x][i]=f[f[x][i-1]][i-1],g[x][i]=min(g[x][i-1],g[f[x][i-1]][i-1]); if(x<=n) minn[x]=dis[x]; else minn[x]=inf;//,printf("minn=%d %lld\n",x,minn[x]);
	for(register int i=h[x],y;i;i=ver[i]) {f[y=to[i]][0]=x; g[y][0]=val[x]; prework(y); minn[x]=min(minn[x],minn[y]);} //minn[x]-=c[x]; printf("minn=%d %lld\n",x,minn[x]);
}
inline int jump(int u,int lim) {for(register int i=20;i>=0;--i) if(f[u][i]&&g[u][i]>lim) u=f[u][i]; return u;}
int main() {
	int T=read();
	while(T--) {
		n=read(); int m=read(),tot=n; ll ans=0;
		cnt=0; for(register int i=1;i<=n;++i) h[i]=0; 
		for(register int i=1;i<=m;++i) {
			e[i].x=read();e[i].y=read();e[i].w1=read();e[i].w2=read();
			add(e[i].x,e[i].y,e[i].w1); add(e[i].y,e[i].x,e[i].w1);
		}
		dijkstra(); std::sort(e+1,e+1+m,cmp); //printf("Over\n");
//		for(register int i=1;i<=m;++i) printf("%d %d\n",e[i].x,e[i].y);
		cnt=0; for(register int i=1;i<=2*n-1;++i) h[i]=0,fa[i]=i;
		for(register int i=1,fx,fy;i<=m;++i) if((fx=find(e[i].x))!=(fy=find(e[i].y))) {val[++tot]=e[i].w2; fa[fx]=fa[fy]=tot; add(tot,fx); add(tot,fy);}
//		for(register int i=1;i<=tot;++i) printf("%d ",find(i)); printf("\n");
		for(register int i=n+1;i<=tot;++i) if(fa[i]==i) prework(i);
		int Q=read(),K=read(),S=read();
//		for(register int i=1;i<=n;++i) printf("%lld ",dis[i]); printf("\n");
	//	for(register int i=1;i<=tot;++i) printf("%lld ",minn[i]); printf("\n"); 
		while(Q--) {
			int v=(read()+K*ans-1)%n+1,p=(read()+K*ans)%(S+1),u=jump(v,p);
			printf("%lld\n",ans=minn[u]);
		}
	}
		
	return 0;
}
这道题实质上求从 \(S_i\) 出发能够到达的点与从 \(E_i\) 出发能够到达点是否存在交集。
没有边权怎么用 \(\mathrm{Kruskal}\) 重构树做呢?赋一个边权就好了(
具体可以参考这篇题解:https://www.cnblogs.com/tommy0103/p/13831833.html
观察一下,设 \(dis_u\) 为机器人从 \(u\) 点走到最近的充电站需要的花费。
显然有以下性质:
- 当 \(x\geq dis_u\) 时,有 \(x'=c-dis_u\geq x\),其中 \(x'\) 表示走到最近的充电站后再回到 \(u\) 点时的电量。
 
如果我们将一条边 \((a,b,w)\) 加入我们构造出的新图 \(G'\),当且仅当 \(c-dis_a-w\geq dis_b\),表示 \(a\) 可以通过这条边走到 \(b\) ,并且 \(b\) 可以到达最近的充电站,根据上述性质,\(b\) 到达充电站后再返回一定优于在 \(a\) 时的电量,那么这样的一条边是可以拓展出去,即对答案产生贡献的。
对 \(c-dis_a-w\geq dis_b\) 进行移项,得到 \(c\geq dis_a+dis_b+w\),即仅当满足 \(c\geq dis_a+dis_b+w\) 时,\(a,b\) 间有边 \((a,b,dis_a+dis_b+w)\) 。这个问题就被转化成了一个连通性问题。使用 \(\text{Kruskal}\) 重构树求询问给出的两点间在新图 \(G'\) 中边的最大值最小的路径即可。

                
            
        
浙公网安备 33010602011771号