介绍
一种在省选题中较平凡的 ( ext{trick})。由建树和重构两部分组成,但最常见的写法是不建树而直接重构,这样能够降低代码复杂度。对于一类只关注连通性的题目,可以起到奇效。对于另一类既关注连通性,又关注最短路径等其他信息的题目,不那么套路,或者根本不可做。
概述
在 ( ext{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
"); 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
",ans=b[res]); else printf("-1
"),ans=-1;
}
return 0;
}
题意:(n) 个点 (m) 条无向边的图,每条边有两个信息 ((l,a)),有 (Q) 个询问, 每次给出出发点 (v_i) 和约束 (p_i)。求从 (v_i) 出发到达 (1) 的最短路径,其中从 (v_i) 出发,(ageq 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
",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
",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
");
// for(register int i=1;i<=m;++i) printf("%d %d
",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("
");
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("
");
// for(register int i=1;i<=tot;++i) printf("%lld ",minn[i]); printf("
");
while(Q--) {
int v=(read()+K*ans-1)%n+1,p=(read()+K*ans)%(S+1),u=jump(v,p);
printf("%lld
",ans=minn[u]);
}
}
return 0;
}
这道题实质上求从 (S_i) 出发能够到达的点与从 (E_i) 出发能够到达点是否存在交集。
没有边权怎么用 (mathrm{Kruskal}) 重构树做呢?赋一个边权就好了(
具体可以参考这篇题解:https://www.cnblogs.com/tommy0103/p/13831833.html
观察一下,设 (dis_u) 为机器人从 (u) 点走到最近的充电站需要的花费。
显然有以下性质:
- 当 (xgeq dis_u) 时,有 (x'=c-dis_ugeq x),其中 (x') 表示走到最近的充电站后再回到 (u) 点时的电量。
如果我们将一条边 ((a,b,w)) 加入我们构造出的新图 (G'),当且仅当 (c-dis_a-wgeq dis_b),表示 (a) 可以通过这条边走到 (b) ,并且 (b) 可以到达最近的充电站,根据上述性质,(b) 到达充电站后再返回一定优于在 (a) 时的电量,那么这样的一条边是可以拓展出去,即对答案产生贡献的。
对 (c-dis_a-wgeq dis_b) 进行移项,得到 (cgeq dis_a+dis_b+w),即仅当满足 (cgeq dis_a+dis_b+w) 时,(a,b) 间有边 ((a,b,dis_a+dis_b+w)) 。这个问题就被转化成了一个连通性问题。使用 ( ext{Kruskal}) 重构树求询问给出的两点间在新图 (G') 中边的最大值最小的路径即可。