克鲁斯卡尔重构树
概念
克鲁斯卡尔重构树,顾名思义,算是克鲁斯卡尔算法的衍生算法。下面给出如何构建克鲁斯卡尔重构树。
1.我们先将边排序,不同的排序规则会使最终的树有不同的性质。
2.排序后遍历每条边,若该边连接的两个点
u
,
v
u,v
u,v不在一个连通块内,我们就将该边作为一个新点
x
x
x,该边的权值作为
x
x
x的点权,然后将
x
x
x作为
f
i
n
d
(
u
)
find(u)
find(u)和
f
i
n
d
(
v
)
find(v)
find(v)的父节点。
3.遍历完所有的边后,就会形成一棵克鲁斯卡尔重构树。
性质
现在我们思考一下这样形成的树有什么性质。
1.初始点全部是叶子节点:因为我们在构造树时只有新点会作为某些点的父节点。
2.若我们初始时将边按从小到大排序,最终形成的树是一个大根堆。
3.若我们初始时将边按从大到小排序,最终形成的树是一个小根堆。
应用
根据性质2,我们可以解决一类问题。给我们一个
n
n
n个点
m
m
m条边的无向图,让我们求从
u
u
u出发只经过边权不超过
x
x
x的边可以到达的结点。
答案就是点
u
u
u的 点权小于等于x的最高的那个祖先 子树中的所有点。
例题1 CF Qpwoeirut and Vertices
题目大意:
给我们一个无向无权图,若干个询问,每次询问给我们一个区间 [ l , r ] [l,r] [l,r],问我们只经过前k条边使得该区间内任意两点可以互相到达的k的最小值。
分析
我们首先将边的边权变为该边的编号,构建克鲁斯卡尔重构树。
对于两个点
u
,
v
u,v
u,v,经过前k条边将他们联通的k的最小值就是他们的最近公共祖先。
接着我们设
f
[
i
]
f[i]
f[i]表示将
i
i
i和
i
+
1
i+1
i+1联通的k的最小值。则将区间
[
l
,
r
]
[l,r]
[l,r]中的所有点联通的k的最小值就是
f
[
l
]
f[l]
f[l]~
f
[
r
−
1
]
f[r-1]
f[r−1]的最大值。
做法
1.将边的编号作为边权从小到大构建克鲁斯卡尔重构树。
2.预处理出
f
f
f数组,使用最近公共祖先。
3.线段树维护区间最大值。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10,M = 4e5+10;
int h[N<<1],e[M],ne[M],idx;
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int n,m,mm;
int a[M],p[M];
int q[M],depth[M],f[M][20],z[M];
int find(int x){
if(p[x]!=x)p[x]=find(p[x]);
return p[x];
}
void bfs(int n){
int hh=0,tt=0;
memset(depth,0x3f,(sizeof (int))*(n+5));
depth[0]=0,depth[n]=n;
q[tt++]=n;
memset(f[n],0,sizeof f[n]);
while(hh!=tt){
int t=q[hh++];
//cout<<t<<endl;
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
if(depth[j]>depth[t]+1){
depth[j]=depth[t]+1;
q[tt++]=j;
f[j][0]=t;
for(int k=1;k<18;k++)
f[j][k]=f[f[j][k-1]][k-1];
}
}
}
}
int LCA(int a,int b){
if(depth[a]<depth[b])swap(a,b);
for(int i=17;i>=0;i--){
if(depth[f[a][i]]>=depth[b])a=f[a][i];
}
if(a==b)return a;
for(int i=17;i>=0;i--){
if(f[a][i]!=f[b][i]){
a=f[a][i];
b=f[b][i];
}
}
return f[a][0];
}
struct Seg{
int l,r;
int mx;
}tr[N<<3];
void pushup(int u){
tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void build(int u,int l,int r){
tr[u]={l,r};
if(l==r){
tr[u].mx=z[l];
return;
}
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r)return tr[u].mx;
int mid=tr[u].l+tr[u].r>>1;
int res=0;
if(mid>=l)res=query(u<<1,l,r);
if(mid<r)res=max(res,query(u<<1|1,l,r));
return res;
}
void solve(){
scanf("%d%d%d",&n,&m,&mm);
memset(h,-1,(sizeof (int))*(n*2+10));
memset(a,0,(sizeof (int))*(n*2+10));
for(int i=1;i<=n;i++)p[i]=i;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
int uu=find(u),vv=find(v);
if(uu==vv)continue;
a[++n]=i;
p[n]=n;
p[uu]=p[vv]=p[n];
add(n,uu),add(n,vv);
}
bfs(n);
for(int i=1;i<n;i++){
int lca=LCA(i,i+1);
z[i]=a[lca];
}
build(1,1,n-1);
while(mm--){
int l,r;
scanf("%d%d",&l,&r);
if(l==r){
printf("0 ");
}
else printf("%d ",query(1,l,r-1));
}
printf("\n");
}
int main(){
int T;
scanf("%d",&T);
while(T--)solve();
return 0;
}
例题2 2021上海站 Life is a Game
题目大意
给我们一个无向有权图。每个点有自己的价值。我们可以在图上来回走,走到一个点后会获得该点的价值,能够重复到达一个点但是不能重复获得该点的价值。但是,从一个点走到另一个点需要满足当前带有的价值要大于边权。若干次询问,每次询问给一个起点和一个价值,问最多可以获得多少价值。
分析
每次询问:我们肯定是先走当前可以走的边,一边走一边获得价值,同时拓展可以走的边,直到任何一个边都不能走。
那我们可以按照边权从小到大构建克鲁斯卡尔重构树,对于一个起点(叶子节点),不断地向上走,每向上走一次,他的价值就会变成所在点的子树的所有叶子节点的价值之和加上最初的价值,不断往上走直到无法继续(当前价值小于父节点的点权)。
但是,如果每次询问我们都一个一个地向上走,对于链式的树肯定会超时。
考虑使用倍增来优化。设
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示从点i向上走
2
i
2^i
2i到达的点,我们设
d
[
i
]
[
j
]
d[i][j]
d[i][j]表示当前位于点
i
i
i,走到
f
[
i
]
[
j
]
f[i][j]
f[i][j]所需的价值。即在点i至少拥有
d
[
i
]
[
j
]
d[i][j]
d[i][j]才能够到达
f
[
i
]
[
j
]
f[i][j]
f[i][j]。
d
[
i
]
[
j
]
d[i][j]
d[i][j]如何计算:
设s[i]表示点i的子树中所有叶子节点之和
我们可以把从
i
i
i走到
f
[
i
]
[
j
]
f[i][j]
f[i][j]分成两部分,先从
i
i
i走到
f
[
i
]
[
j
−
1
]
f[i][j-1]
f[i][j−1],再从
f
[
i
]
[
j
−
1
]
f[i][j-1]
f[i][j−1]走到
f
[
i
]
[
j
]
f[i][j]
f[i][j],设初始时有k点价值,现在我们从
f
[
i
]
[
j
−
1
]
f[i][j-1]
f[i][j−1] 走到
f
[
i
]
[
j
]
f[i][j]
f[i][j] 需要
d
[
f
[
i
]
[
j
−
1
]
]
[
j
−
1
]
d[f[i][j-1]][j-1]
d[f[i][j−1]][j−1] ,即有如下不等式:
s [ f [ i ] [ j − 1 ] ] + k > = d [ f [ i ] [ j − 1 ] ] [ j − 1 ] s[f[i][j-1]]+k>=d[f[i][j-1]][j-1] s[f[i][j−1]]+k>=d[f[i][j−1]][j−1]
则 k > = d [ f [ i ] [ j − 1 ] ] [ j − 1 ] − s [ f [ i ] [ j − 1 ] ] k>=d[f[i][j-1]][j-1]-s[f[i][j-1]] k>=d[f[i][j−1]][j−1]−s[f[i][j−1]]
那么我们现在处于点
i
i
i,当前拥有的价值为
s
[
i
]
+
k
s[i]+k
s[i]+k。
将上面的不等式代入则有
s
[
i
]
+
k
>
=
s
[
i
]
+
d
[
f
[
i
]
[
j
−
1
]
]
[
j
−
1
]
−
s
[
f
[
i
]
[
j
−
1
]
]
s[i]+k>=s[i]+d[f[i][j-1]][j-1]-s[f[i][j-1]]
s[i]+k>=s[i]+d[f[i][j−1]][j−1]−s[f[i][j−1]]。
同时由于我们需要先从 i i i走到 f [ i ] [ j − 1 ] f[i][j-1] f[i][j−1],所以还需要满足 s [ i ] + k > = d [ i ] [ j − 1 ] s[i]+k>=d[i][j-1] s[i]+k>=d[i][j−1]。
故 d [ i ] [ j ] = m a x ( s [ i ] + d [ f [ i ] [ j − 1 ] ] [ j − 1 ] − s [ f [ i ] [ j − 1 ] ] , d [ i ] [ j − 1 ] ) d[i][j]=max(s[i]+d[f[i][j-1]][j-1]-s[f[i][j-1]],d[i][j-1]) d[i][j]=max(s[i]+d[f[i][j−1]][j−1]−s[f[i][j−1]],d[i][j−1])。
那么每次询问的答案如何计算:
倍增的不断向上跳即可,若最后处于点
i
i
i,答案就是
i
i
i的子树中所有叶子节点的价值之和加上初始价值。
做法
1.将边从小到大排序后构建克鲁斯卡尔重构树。
2.
d
f
s
dfs
dfs求出每个点的子树中所有叶子节点价值之和。
3.
b
f
s
bfs
bfs预处理出
f
f
f数组和
d
d
d数组。
4.每次询问倍增地向上跳。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5+10;
int h[N],e[N],ne[N],idx;
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int n,m,mm;
ll a[N];
int p[N];
struct Edge{
int u,v,w;
}edge[N];
bool operator<(const Edge&e1,const Edge&e2){
return e1.w<e2.w;
}
int find(int x){
if(x!=p[x])p[x]=find(p[x]);
return p[x];
}
int depth[N],f[N][20];
ll d[N][20];
ll s[N];
int q[N];
void dfs(int u){
s[u]=0;
if(h[u]==-1)s[u]=a[u];
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(!s[j]){
dfs(j);
s[u]+=s[j];
}
}
}
void bfs(){
int hh=0,tt=0;
memset(depth,0x3f,sizeof depth);
depth[0]=0;depth[n]=1;
for(int i=0;i<18;i++)f[n][i]=n;
q[tt++]=n;
while(hh!=tt){
int t=q[hh++];
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
if(depth[j]>depth[t]+1){
depth[j]=depth[t]+1;
q[tt++]=j;
f[j][0]=t;
d[j][0]=a[t];
for(int k=1;k<18;k++){
f[j][k]=f[f[j][k-1]][k-1];
d[j][k]=max(d[f[j][k-1]][k-1]-s[f[j][k-1]]+s[j],d[j][k-1]);
}
}
}
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m>>mm;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)p[i]=i;
for(int i=1;i<=m;i++){
cin>>edge[i].u>>edge[i].v>>edge[i].w;
}
sort(edge+1,edge+1+m);
for(int i=1;i<=m;i++){
int u=find(edge[i].u),v=find(edge[i].v);
if(u!=v){
a[++n]=edge[i].w;
p[n]=n;
add(n,u),add(n,v);
p[u]=p[v]=n;
}
}
dfs(n);
bfs();
while(mm--){
int x,k;
cin>>x>>k;
for(int i=17;i>=0;i--){
if(s[x]+k>=d[x][i]){
x=f[x][i];
}
}
cout<<s[x]+k<<endl;
}
return 0;
}