Kruskal重构树学习笔记
这里是Kruskal重构树学习笔记。
Kruskal重构树,是用于求出有关一张图中,某点仅经过边权 某个值 的边所得到的子图的有关信息的工具。
但事实上,其应用还有更多。
我们先讲述其构造方法:
-
将所有边按照边权递增排序。
-
依次枚举每一条边。假如此时边的两个端点处于两个不同集合中,按照常规Kruskal算法,此时应该将该边加入MST,并在dsu上合并这两个点;然而,在Kruskal重构树算法中,我们将先新建一个点,然后将 所在子树的根当作该新建点的两个儿子。同时,将该新建点的权值设作该边的边权。
具体而言,假设我们有三个点,以及三条边 。下面我们构造重构树:
-
第一条边 ,两个端点 不位于同一子树中,故新建点 ,点权为 ,两个儿子为 和 。
-
第二条边 ,两个端点 不位于同一子树中,故新建点 。此时, 所在子树的根是 , 所在子树的根是 ,故连边 ,并将 的点权设作 。
-
第三条边 ,两个端点 已都位于 的子树中,故不作任何操作。
就此我们构造出了Kruskal重构树。
它有什么性质呢?
- 其成一个大根堆
因为我们加入的边权递增,故一个点的权值一定大于其两个儿子的权值,假如我们把原图中点的权值设作 的话。
- 原图中两点间所有路径中,路径上所有边的最大值最小的那一条路径的上述最大值为重构树上两点LCA的权值。
首先,路径上所有边的最大值最小的路径,一定是MST上的路径;而重构树上两点间路径唯一等价于MST上路径;又重构树有大根堆性质,故最大值一定出现在LCA处。
这也就给我们提供了截出到 只经过权值不超过 的点集的方法:在重构树上树上倍增找到最后一个权值不大于 的 的祖先 ,然后 子树中所有节点即为该点集。因为 到 子树内任何点的LCA一定也在 的子树内, 到 子树外任何点的LCA一定也在 的子树外,按照大根堆性质,前者一定在点集内,后者一定在点集外。
Kruskal重构树的复杂度为 ,来自于排序和不能按秩合并,只能路径压缩的冰茶姬。
I.Peaks
可以被直接当作模板了。
树上倍增到需要的节点,这样所有合法的点集就变成了重构树上某节点的子树中所有节点,也即dfs序上一段区间。于是问题变为经典的区间 大数问题,直接主席树带走。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,q,a[100100],dsu[200100],num,val[200100],anc[200100][20],L[200100],R[200100],rev[100100],cnt,rt[100100],tot;
struct node{
int x,y,z;
friend bool operator<(const node&u,const node&v){return u.z<v.z;}
void read(){scanf("%d%d%d",&x,&y,&z);}
}e[500100];
vector<int>v[200100],u;
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void dfs(int x,int fa){
anc[x][0]=fa;
if(v[x].empty())L[x]=R[x]=++tot,rev[tot]=x;
else L[x]=tot+1,dfs(v[x][0],x),dfs(v[x][1],x),R[x]=tot;
}
int doubling(int x,int k){
for(int i=19;i>=0;i--)if(anc[x][i]&&val[anc[x][i]]<=k)x=anc[x][i];
return x;
}
struct SegTree{int lson,rson,sum;}seg[3201000];
#define mid ((l+r)>>1)
void modify(int &x,int y,int l,int r,int P){
if(l>P||r<P)return;
x=++cnt,seg[x]=seg[y],seg[x].sum++;
if(l!=r)modify(seg[x].lson,seg[y].lson,l,mid,P),modify(seg[x].rson,seg[y].rson,mid+1,r,P);
}
int query(int x,int y,int l,int r,int k){
if(l==r)return u[l-1];
if(seg[seg[x].lson].sum-seg[seg[y].lson].sum>=k)return query(seg[x].lson,seg[y].lson,l,mid,k);
else return query(seg[x].rson,seg[y].rson,mid+1,r,k-(seg[seg[x].lson].sum-seg[seg[y].lson].sum));
}
int main(){
scanf("%d%d%d",&n,&m,&q),num=n;
for(int i=1;i<=n;i++)scanf("%d",&a[i]),dsu[i]=i,u.push_back(a[i]);
for(int i=1;i<=m;i++)e[i].read();
sort(e+1,e+m+1);
for(int i=1;i<=m;i++){
int x=find(e[i].x),y=find(e[i].y);
if(x==y)continue;
num++,dsu[x]=dsu[y]=dsu[num]=num,val[num]=e[i].z;
v[num].push_back(x),v[num].push_back(y);
}
for(int i=1;i<=num;i++)if(dsu[i]==i)dfs(i,0);
sort(u.begin(),u.end()),u.resize(m=unique(u.begin(),u.end())-u.begin());
for(int i=1;i<=n;i++)a[i]=lower_bound(u.begin(),u.end(),a[i])-u.begin()+1;
for(int j=1;j<=19;j++)for(int i=1;i<=num;i++)anc[i][j]=anc[anc[i][j-1]][j-1];
for(int i=1;i<=n;i++)modify(rt[i],rt[i-1],1,m,a[rev[i]]);
for(int i=1,x,y,z;i<=q;i++){
scanf("%d%d%d",&x,&y,&z);
x=doubling(x,y);
// for(int j=L[x];j<=R[x];j++)printf("%d ",rev[j]);puts("");
if(R[x]-L[x]+1<z){puts("-1");continue;}
printf("%d\n",query(rt[R[x]],rt[L[x]-1],1,m,R[x]-L[x]+2-z));
}
return 0;
}
II.[NOI2018] 归程
建出重构树,则问题转变为子树中所有点到 的距离的最小值。于是预先 Dijkstra 跑一遍求出所有点到 的最短路径,然后再遍历重构树求出子树最短路径,然后回答询问时就直接树上倍增即可。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int T,n,m,ch[400100][2],dis[400100],num,anc[400100][20],dsu[400100],val[400100],Q,K,S,lasans;
vector<pair<int,int> >v[200100];
struct node{
int x,y,z;
friend bool operator<(const node&u,const node&v){return u.z>v.z;}
node(){}
node(int X,int Y,int Z){x=X,y=Y,z=Z;}
}e[400100];
priority_queue<pair<int,int> >q;
void Dijkstra(){
q.push(make_pair(0,1)),dis[1]=0;
while(!q.empty()){
int x=q.top().second;q.pop();
for(auto y:v[x])if(dis[y.first]>dis[x]+y.second)dis[y.first]=dis[x]+y.second,q.push(make_pair(-dis[y.first],y.first));
}
}
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void dfs(int x,int fa){anc[x][0]=fa;if(x>n)dfs(ch[x][0],x),dfs(ch[x][1],x),dis[x]=min(dis[ch[x][0]],dis[ch[x][1]]);}
int doubling(int x,int k){for(int i=19;i>=0;i--)if(anc[x][i]&&val[anc[x][i]]>k)x=anc[x][i];return x;}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m),memset(dis,0x7f,sizeof(dis)),num=n;
for(int i=1,x,y,z,w;i<=m;i++)scanf("%d%d%d%d",&x,&y,&z,&w),v[x].push_back(make_pair(y,z)),v[y].push_back(make_pair(x,z)),e[i]=node(x,y,w);
Dijkstra();
sort(e+1,e+m+1);
for(int i=1;i<=n;i++)dsu[i]=i;
for(int i=1;i<=m;i++){
int x=find(e[i].x),y=find(e[i].y);
if(x==y)continue;
num++,dsu[x]=dsu[y]=dsu[num]=num,val[num]=e[i].z;
ch[num][0]=x,ch[num][1]=y;
}
for(int i=1;i<=n;i++)v[i].clear();
dfs(num,0);
for(int j=1;j<=19;j++)for(int i=1;i<=num;i++)anc[i][j]=anc[anc[i][j-1]][j-1];
scanf("%d%d%d",&Q,&K,&S),lasans=0;
for(int i=1,x,y;i<=Q;i++){
scanf("%d%d",&x,&y),x=(x+K*lasans-1)%n+1,y=(y+K*lasans)%(S+1);
printf("%d\n",lasans=dis[doubling(x,y)]);
}
}
return 0;
}
III.[ARC098D] Donation
Kruskal重构树还可以用来优化DP哦~~
结论1.最优情形下,当我们向一个节点”捐赠“后,我们不会再次访问该节点。
明显,如果再次访问,则在再次访问时再捐献一定不更劣。
我们定义 ,则约束等价于离开 点时剩余钱数不小于 。
明显我们肯定是希望从大到小地依次给每个 捐赠,因为反正离开 时无论如何都会剩下 ,如果先给大的捐赠,这剩下的钱说不定就在之后捐出去了;但是如果后给大的 捐赠,这剩下的钱就有可能烂在手里捐不出去了。
但是,这只是希望,并不代表我们真的做得到,因为在从一个 到另一个 的过程中,我们说不定会路过一个拥有更大 的点 而导致需要更多的入场费。
但是,我们发现,从 到 的本质是让路径最大值最小,同Kruskal重构树的本质一致。
为了让重构树也可以应用于点权,我们将连接两点 的边权设作 。这样,我们便可建出重构树来。
设对于重构树上一个非原图节点,其 为建出重构树时的边权。
设 表示若我们给重构树上节点 的子树内所有节点全都被捐赠过,所需要的最小钱数。
明显,根据结论1,我们捐赠的路径一定是 ,不可能出现两个儿子子树内的节点全捐完后再来捐 的情形,因为从一个儿子中节点走到另一个儿子中节点的最优路径必经过 ,故该路径需要的最大剩余钱数也是 ,而在一个儿子内部乱跑的最大钱数一定不大于 。
因为我们离开 时剩余钱数一定不小于 ,而凭借着这么多钱在第一个儿子里一定可以随便乱跑而不必多带钱,因此设 表示 节点子树中所有节点的 之和,则从第一个儿子 出来时所需费用一定仅需 即可。然后,处理 本身以及另一个儿子 所需费用,为 ,因为要保证经过 的过路限制以及付给 子树中点的费用。于是总代价为 。同理, 也是一条合法路径,也可以贡献为备选答案。
对于叶子节点,其 直接设作 即可。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,b[100100],c[100100],dsu[200100],num,val[200100],ch[400100][2];
ll f[200100],s[200100];
struct node{
int x,y,z;
friend bool operator<(const node&u,const node&v){return u.z<v.z;}
void read(){scanf("%d%d",&x,&y),z=max(c[x],c[y]);}
}e[100100];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void dfs(int x){
if(x<=n)f[x]=b[x]+c[x],s[x]=b[x];
else dfs(ch[x][0]),dfs(ch[x][1]),f[x]=min(s[ch[x][0]]+max(1ll*val[x],f[ch[x][1]]),s[ch[x][1]]+max(1ll*val[x],f[ch[x][0]])),s[x]=s[ch[x][0]]+s[ch[x][1]];
}
int main(){
scanf("%d%d",&n,&m),num=n;
for(int i=1,A,B;i<=n;i++)scanf("%d%d",&A,&B),b[i]=B,c[i]=max(0,A-B);
for(int i=1;i<=m;i++)e[i].read();
sort(e+1,e+m+1);
for(int i=1;i<=n;i++)dsu[i]=i;
for(int i=1;i<=m;i++){
int x=find(e[i].x),y=find(e[i].y);
if(x==y)continue;
num++,dsu[x]=dsu[y]=dsu[num]=num,val[num]=e[i].z;
ch[num][0]=x,ch[num][1]=y;
}
dfs(num);
printf("%lld\n",f[num]);
return 0;
}
IV.[IOI2018] werewolf 狼人
明显在重构树上,所有人能到的点集是dfs序上一段区间,狼能到的点集也是一段区间(注意这里是两棵不同的树,人树的边权是两边的 ,且求最小重构树;而狼树的边权是两边 ,求的是最大重构树)。而我们要判断两段区间里是否有相同的数。
于是对于任意一棵树的dfs序上每个位置,求出其元素在另一棵树上对应的dfs序,然后问题就转换为二维数点问题,判断矩形内是否有元素。简单离线后BIT即可。
但是IOI赛场上是交互式,因此强制在线,故要用主席树。
但是LG上不需要。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,q,a[200100];
struct EDGE{
int u,v,w;
EDGE(){}
EDGE(int U,int V,int W){u=U,v=V,w=W;}
};
struct MST{
EDGE e[400100];
vector<int>v[400100];
int dsu[400100],tot,val[400100],anc[400100][20],L[400100],R[400100],cnt,dfn[200100];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void dfs(int x){
L[x]=cnt+1;if(x<=n)dfn[++cnt]=x;
for(auto y:v[x])anc[y][0]=x,dfs(y);
R[x]=cnt;
}
int doubling(int x,int lim){for(int i=19;i>=0;i--)if(anc[x][i]&&val[anc[x][i]]<=lim)x=anc[x][i];return x;}
void Kruskal(){
tot=n;
sort(e+1,e+m+1,[](EDGE x,EDGE y){return x.w<y.w;});
for(int i=1;i<=n;i++)dsu[i]=i;
for(int i=1;i<=m;i++){
int X=find(e[i].u),Y=find(e[i].v);
if(X==Y)continue;
val[++tot]=e[i].w;
dsu[tot]=dsu[X]=dsu[Y]=tot;
v[tot].push_back(X),v[tot].push_back(Y);
}
// for(int i=1;i<=tot;i++)printf("%d ",val[i]);puts("");
dfs(tot);
for(int j=1;j<=19;j++)for(int i=1;i<=tot;i++)anc[i][j]=anc[anc[i][j-1]][j-1];
// for(int i=1;i<=n;i++)printf("%d ",dfn[i]);puts("");
// for(int i=1;i<=tot;i++)printf("[%d,%d]",L[i],R[i]);puts("");
}
}mnst,mxst;
int pos[200100],val[200100],res[200100],t[200100];
void ADD(int x){while(x<=n)t[x]++,x+=x&-x;}
int SUM(int x){int ret=0;while(x)ret+=t[x],x-=x&-x;return ret;}
vector<pair<int,int> >v[200100];
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),x++,y++,mnst.e[i]=EDGE(x,y,max(x,y)),mxst.e[i]=EDGE(x,y,-min(x,y));
mnst.Kruskal(),mxst.Kruskal();
for(int i=1;i<=n;i++)pos[mnst.dfn[i]]=i;
for(int i=1;i<=n;i++)val[i]=pos[mxst.dfn[i]];
for(int i=1,s,t,l,r,x;i<=q;i++){
scanf("%d%d%d%d",&s,&t,&l,&r),s++,t++,l++,r++;
x=mxst.doubling(s,-l);int L1=mxst.L[x]-1,R1=mxst.R[x];//printf("%d:[%d,%d]\n",x,L1+1,R1);
x=mnst.doubling(t,r);int L2=mnst.L[x]-1,R2=mnst.R[x];//printf("%d:[%d,%d]\n",x,L2+1,R2);
v[L1].push_back(make_pair(L2,i));
v[L1].push_back(make_pair(R2,-i));
v[R1].push_back(make_pair(L2,-i));
v[R1].push_back(make_pair(R2,i));
}
for(int i=1;i<=n;i++){
ADD(val[i]);
for(auto j:v[i])if(j.second>0)res[j.second]+=SUM(j.first);else res[-j.second]-=SUM(j.first);
}
for(int i=1;i<=q;i++)printf("%d\n",!!res[i]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?