(笔记)最小生成树 Kruskal 重构树
参考资料:Kruskal 重构树学习笔记
Kruskal
将边按照边权升序排序,依次尝试加入边并用并查集判断维护是否有环,瓶颈在于 sort,时间复杂度 \(O(m\log m)\)。
Prim
维护一个联通点集 MST,任选一点作为起点加入集合 MST,集合中每个点具有键值(代表该点距离 MST 集合中任意一点的最小值),每次取出集合中键值最小的点,并用它连出的边更新邻居的键值。若更新键值比记录的先前键值要小,那么将其加入集合。该集合可以用 STL 优先队列(二分堆)维护,注意到一个点第一次被访问一定是键值最小的时候,堆维护的信息是一个 pair \((dis_x,x)\),\(dis_x\) 即为键值,且一个节点可能被多次入堆,那么我们只需要管第一次就好,时间复杂度 \(O((n+m)\log m)\),看不出哪里很优秀。
UPD:CSP-S 2025 T2 因为 \(n\) 较小考场尝试手搓这个东西然后一败涂地。
Kruskal 重构树
适用于边有边权,求图上两点所有路径边最大权值的最小值问题,该结构具有如下良好性质:
-
是一棵二叉树。
-
如果是按最小生成树建立的话是一个大根堆。
-
强大性质:原图中两个点间所有路径上的边最大权值的最小值 \(=\) 最小生成树上两点简单路径的边最大权值 \(=\) Kruskal 重构树上两点 LCA 的点权。
建树方式如下:先找出最小生成树上所有边,然后按边权从小到大排序,利用并查集维护节点 \(u\) 在 Kruskal 重构树上的所属树根 \(fa_u\),每次处理 \(x\to y\) 上的边新建一个节点 \(z\) 点权 \(val_z\) 为原边权,然后连接 \(z\to fa_u,z\to fa_v\),并使 \(fa_u,fa_v\to z\),即在并查集上与 \(z\) 合并。
例题
[AGC002D] Stamp Rally
一个节点走不超过 \(v\) 的边权能走到的最多节点数量,实际上就是在 \(u\) 的祖先链上找到最浅的满足 \(val_u\geq v\) 的节点的子树中的叶子节点数量。有了这个我们就可以对每次询问在线回答,二分套倍增即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int n,m,ncnt,val[N*2];
struct edge{int u,v;}E[N];
vector<int>G[N*2];
int fa[N*2];
bool us[N*2];
inline int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
bool ins(int x,int y){
int frx=fr(x),fry=fr(y);
if(frx==fry)return 0;
fa[frx]=fry;return 1;
}
int f[N*2][18],dep[N*2],siz[N*2];
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int v:G[u]){
if(v==fa)continue;
f[v][0]=u;
for(int j=1;(1<<j)<=dep[u];j++)
f[v][j]=f[f[v][j-1]][j-1];
dfs(v,u);
siz[u]+=siz[v];
}
if(!siz[u])siz[u]=1;
}
bool check(int x,int y,int &z,int &mid){
for(int i=17;i>=0;i--){
if(val[f[x][i]]<=mid)
x=f[x][i];
if(val[f[y][i]]<=mid)
y=f[y][i];
}
if(x==y)return siz[x]>=z;
return (siz[x]+siz[y])>=z;
}
int ef(int &x,int &y,int &z){
int l=1,r=m,res=m;
while(l<=r){
int mid=(l+r)>>1;
if(check(x,y,z,mid))res=mid,r=mid-1;
else l=mid+1;
}
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;ncnt=n;val[0]=1e9;
for(int i=1;i<=n*2;i++)fa[i]=i;
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
u=fr(u),v=fr(v);
if(u!=v){
val[++ncnt]=i;
G[u].push_back(ncnt);
G[ncnt].push_back(u);
G[v].push_back(ncnt);
G[ncnt].push_back(v);
fa[u]=fa[v]=ncnt;
}
}
dfs(ncnt,0);
int qn;cin>>qn;
for(int i=1;i<=qn;i++){
int x,y,z;cin>>x>>y>>z;
cout<<ef(x,y,z)<<'\n';
}
return 0;
}
P4899 [IOI 2018] werewolf 狼人
上下界 Kruskal 重构树,思路挺巧妙的,或者换个字眼,挺套路的。没看题解自己口胡了一下,大概针对最小生成树和最大生成树分别建一个 Kruskal 重构树,前者的边权需要是 \(\max(u,v)\),后者则是 \(\min(u,v)\)(这样就能保证在上面倍增找节点的时候,子树内的所有叶子节点编号都符合标号要求)。然后发现每个点会在两棵树上分别有个 DFS 序,然后问题实际上就转化为了分别在两棵树上找出了对应的只走 \(\le\) 或 \(\geq\) 一定值所能走到的所有点的集合(在对应树上构成一段连续的 DFS 序区间,可以只计算叶子节点),我们需要判断找到的这两个集合里面是否有重复的点(作为中间点)。然后就转化成了一个类似 \(L_1\le dfn_u\le R_1\land L_2\le dfn'_u\le R_2\) 的形态,这个东西可以直接离线二维数点做,然后就解决了。
对于没见过这种套路的人来说,想到还真不容易。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=4e5+5,INF=1e9;
int n,m,qn;
struct Kruskal{
vector<int>G[N];
int fa[N],ncnt,val[N],f[N][21],dfn[N],tms,dep[N],siz[N];
inline int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
void init(){ncnt=n;for(int i=1;i<=2*n;i++)fa[i]=i;}
inline void link(int y){
fa[y]=ncnt;
G[ncnt].push_back(y);
}
void dfs(int u){
if(u==ncnt)dep[u]=1;
if(u<=n)dfn[u]=++tms;
else dfn[u]=INF;
for(int v:G[u]){
f[v][0]=u;
dep[v]=dep[u]+1;
for(int i=1;(1<<i)<=dep[u];i++)
f[v][i]=f[f[v][i-1]][i-1];
dfs(v);
if(u>n)dfn[u]=min(dfn[u],dfn[v]);
siz[u]+=siz[v];
}
if(u<=n)siz[u]=1;
}
pair<int,int>find(int x,int v,bool tf){
for(int i=20;i>=0;i--){
if(!tf&&val[f[x][i]]<=v)x=f[x][i];
else if(tf&&val[f[x][i]]>=v)x=f[x][i];
}
return make_pair(dfn[x],dfn[x]+siz[x]-1);
}
}T1,T2;
struct edge{int u,v;}e[N<<1];
bool cmp1(edge x,edge y){return max(x.u,x.v)<max(y.u,y.v);}
bool cmp2(edge x,edge y){return min(x.u,x.v)>min(y.u,y.v);}
struct Tre{
int av[N];
inline int lowbit(int x){return x&-x;}
void ins(int p,int x){for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;}
int que(int p){int res=0;for(int i=p;i;i-=lowbit(i)){res+=av[i];}return res;}
}T;
int cntq;
struct Q{int pos,l,r,op,id;}q[N<<2];
bool cmp(Q x,Q y){return x.pos<y.pos;}
vector<int>POS[N];
int ans[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>qn;
T1.init();T2.init();
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
u++,v++;
e[i]=(edge){u,v};
}
sort(e+1,e+1+m,cmp1);
T1.val[0]=INF;
for(int i=1;i<=m;i++){
int u=e[i].u,v=e[i].v;
u=T1.fr(u);v=T1.fr(v);
if(u!=v){
T1.val[++T1.ncnt]=max(e[i].u,e[i].v);
T1.link(u);T1.link(v);
}
}
sort(e+1,e+1+m,cmp2);
for(int i=1;i<=m;i++){
int u=e[i].u,v=e[i].v;
u=T2.fr(u);v=T2.fr(v);
if(u!=v){
T2.val[++T2.ncnt]=min(e[i].u,e[i].v);
T2.link(u);T2.link(v);
}
}
T1.dfs(T1.ncnt);
T2.dfs(T2.ncnt);
for(int i=1;i<=n;i++)
POS[T1.dfn[i]].push_back(T2.dfn[i]);
for(int i=1;i<=qn;i++){
int s,e,l,r;
cin>>s>>e>>l>>r;
s++,e++,l++,r++;
pair<int,int>res=T2.find(s,l,1);
int L2=res.first,R2=res.second;
res=T1.find(e,r,0);
int L1=res.first,R1=res.second;
q[++cntq]=(Q){L1-1,L2,R2,-1,i};
q[++cntq]=(Q){R1,L2,R2,1,i};
}
int pos=1;
sort(q+1,q+1+cntq,cmp);
for(int i=1;i<=cntq;i++){
while(pos<=q[i].pos){
for(int v:POS[pos])
T.ins(v,1);
pos++;
}
ans[q[i].id]+=q[i].op*(T.que(q[i].r)-T.que(q[i].l-1));
}
for(int i=1;i<=qn;i++)
cout<<(ans[i]>0)<<'\n';
return 0;
}
P3665 [USACO17OPEN] Switch Grass P
一类比较牛的应用,反正场上没想出来。考虑把合法边限制在最小生成树上后,考虑 Kruskal 重构树的合并过程,发现建成一个大根堆形式的该点有效条件当且仅当两棵子树内部颜色都一样(否则内部匹配更优),然后左右两棵子树不同色。
任意一个点都可以代表整棵子树颜色,所以整个结构可以根据 Kruskal 重构树合并成一个等效链,每次修改只需要顾及最多 \(2\) 条边,用一个 STL multiset 维护 \(\min\) 即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=4e5+5;
int n,m,K,Q,c[N],fa[N],L[N],R[N],ncnt;
multiset<int>s;
vector<PII>G[N];
struct Edge{int u,v,w;}e[N];
inline bool cmp(Edge x,Edge y){return x.w<y.w;}
inline int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
int main(){
//freopen("d.in","r",stdin);
//freopen("d.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>K>>Q;ncnt=n;
for(int i=1;i<=2*n;i++)
fa[i]=i,L[i]=R[i]=i;
for(int i=1;i<=m;i++){
int u,v,w;cin>>u>>v>>w;
e[i]=Edge{u,v,w};
}
for(int i=1;i<=n;i++)
cin>>c[i];
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++)
if(fr(e[i].u)!=fr(e[i].v)){
int fru=fr(e[i].u),frv=fr(e[i].v);
fa[fru]=fa[frv]=++ncnt;
G[R[fru]].emplace_back(make_pair(L[frv],e[i].w));
G[L[frv]].emplace_back(make_pair(R[fru],e[i].w));
if(c[R[fru]]!=c[L[frv]])
s.insert(e[i].w);
L[ncnt]=L[fru],R[ncnt]=R[frv];
}
while(Q--){
int u,k;cin>>u>>k;
if(c[u]!=k){
for(PII j:G[u]){
int v=j.first,w=j.second;
if(c[v]!=c[u])s.erase(s.find(w));
if(c[v]!=k)s.insert(w);
}
c[u]=k;
}
cout<<(*s.begin())<<'\n';
}
return 0;
}

浙公网安备 33010602011771号