先推荐一篇对我帮助很大的博客:Kruskal重构树入门
前置知识
1、(Kruskal)算法
2、基础数据结构,如主席树
简述
(Kruskal)重构树与(Kruskal)算法密切相关。
我们知道,(Kruskal)算法是按照边权排序,依次合并节点,并用并查集维护联通。
(Kruskal)重构树则是合并节点(x,y)时,断开(x,y)的连边,新建一个节点(z),将(z)作为(x,y)的父亲,同时把((x,y))的边权作为(z)的点权,然后用并查集维护联通。
那么它有一些很有用的性质。
1、它是一个二叉堆。
2、若边权升序,则它是一个大根堆
3、任意两点路径边权最大值为(Kruskal)重构树上(LCA)的点权。
实现大致如下:
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void Kruskal()
{
sort(E+1, E+m+1, cmp);
for (int i=1; i<=n; i++) fa[i]=i; int now=n;
for (int i=1; i<=m; i++)
{
int x=E[i].x, y=E[i].y, w=E[i].w;
int fx=find(x), fy=find(y);
if (fx^fy)
{
val[++now]=w; fa[fx]=fa[fy]=fa[now]=now;
add(now, fx, 1); add(now, fy, 1);
}
}
}
例题
我们来看一道例题:
bzoj3551 Peaks
你没发现这是两个链接吗
Description
(N)座山峰,有高度(h_i)。山峰间有(M)条双向边,有边权。(Q)组询问,询问从(v)开始经过边权不超过(x)能到达的山峰中第(k)高。强制在线。
Solution
显然,一个点能到达,在最小生成树上必定能到达。但建出最小生成树并没有什么用,于是我们建出(Kruskal)重构树,然后发现(dfs)+主席树静态区间(K)大就可以了。
代码如下(本代码为洛谷4197AC代码):
#include<bits/stdc++.h>
using namespace std;
const int N=200005, M=500005;
struct node{int x, y, w;}E[M];
struct NODE{int to, nxt;}edge[N];
int size[N], deg[N], fa[N], dfn[N];
int in[N], out[N], h[N>>1], val[N];
int f[N][30], head[N], cnt, Cnt, tot, n, m, q;
int rt[N], ls[N<<5], rs[N<<5], sum[N<<5];
inline int read()
{
int x=0,f=1;char ch=getchar();
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
return x*f;
}
void add(int u, int v)
{
edge[++Cnt]=(NODE){v, head[u]};
head[u]=Cnt; deg[v]++;
}
bool cmp(node a, node b){return a.w<b.w;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void dfs(int u)
{
dfn[++tot]=u; in[u]=tot;
for (int i=1; (1<<i)<=(n<<1); i++) f[u][i]=f[f[u][i-1]][i-1];
for (int i=head[u]; i; i=edge[i].nxt)
{
int v=edge[i].to; if (v==f[u][0]) continue;
f[v][0]=u; dfs(v); size[u]+=size[v];
}
if (!size[u]) size[u]=1; out[u]=tot;
}
void insert(int &o, int l, int r, int x, int v)
{
ls[++cnt]=ls[o]; rs[cnt]=rs[o];
sum[cnt]=sum[o]+v; o=cnt;
if (l==r) return; int mid=(l+r)>>1;
if (x<=mid) insert(ls[o], l, mid, x, v);
else insert(rs[o], mid+1, r, x, v);
}
int kth(int u, int v, int l, int r, int k)
{
if (l==r) return l;
int mid=(l+r)>>1, s=sum[rs[v]]-sum[rs[u]];
if (k<=s) return kth(rs[u], rs[v], mid+1, r, k);
else return kth(ls[u], ls[v], l, mid, k-s);
}
int query(int v, int x, int k)
{
for (int i=25; ~i; i--)
if (f[v][i] && val[f[v][i]]<=x) v=f[v][i];
if (size[v]<k) return -1;
return kth(rt[in[v]-1], rt[out[v]], 0, 1e9, k);
}
int main()
{
n=read(); m=read(); q=read(); int nn=n;
for (int i=1; i<=n; i++) h[i]=read();
for (int i=1; i<=m; i++) E[i]=(node){read(), read(), read()};
sort(E+1, E+m+1, cmp);
for (int i=1; i<=(n<<1); i++) fa[i]=i;
for (int i=1; i<=m; i++)
{
int fx=find(E[i].x), fy=find(E[i].y);
if (fx^fy)
{
val[++nn]=E[i].w; fa[fx]=fa[fy]=nn;
add(nn, fx); add(nn, fy);
}
}
for (int i=1; i<=nn; i++) if (!deg[i]) dfs(i);
for (int i=1; i<=nn; i++)
{
rt[i]=rt[i-1];
if (dfn[i]<=n) insert(rt[i], 0, 1e9, h[dfn[i]], 1);
}
for (int i=1; i<=q; i++)
{
int v=read(), x=read(), k=read();
printf("%d
", query(v, x, k));
}
return 0;
}
再看一题
NOI2018 归程
机房神犇(zzr)用200+行可持久化并查集大力秒题,可是我不会。
Description
(N)个点(M)条边的连通图,用(l, a)表示长度,海拔。
(Q)次询问,每次询问给定起点(v ),水位线(p),求经过海拔不低于(p)能到达的点中距离1号节点最近的距离。
Solution
建立以海拔为关键字的(Kruskal)重构树(海拔为降序),那么能到达的点在(Kruskal)重构树的一个子树中。
那么就很清晰了,先用(Dijkstra)预处理单源最短路,再构建(Kruskal)重构树,然后(dfs)维护每个店为根的子树中距1号点的最小距离。处理询问时树上倍增即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=800005;
struct node{int x, y, l;}E[N];
struct Edge{int to, nxt, w;}edge[N];
struct heap{int u, w;};
int f[N][21], fa[N], vis[N], dis[N], mn[N], val[N];
int head[N], cnt, n, m;
bool operator < (heap a, heap b){return a.w>b.w;}
bool cmp(node a, node b){return a.l>b.l;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
inline int read()
{
int x=0,f=1;char ch=getchar();
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
return x*f;
}
void add(int u, int v, int w)
{
edge[++cnt]=(Edge){v, head[u], w};
head[u]=cnt;
}
void Dijkstra()
{
memset(vis, 0, sizeof(vis)); vis[1]=1;
memset(dis, 0x3f, sizeof(dis)); dis[1]=0;
priority_queue<heap> q; q.push((heap){1, 0});
while (!q.empty())
{
int u=q.top().u; q.pop();
for (int i=head[u]; i; i=edge[i].nxt)
{
int v=edge[i].to, w=edge[i].w;
if (dis[v]>dis[u]+w)
dis[v]=dis[u]+w, q.push((heap){v, dis[v]});
}
}
}
void dfs(int u, int fa)
{
f[u][0]=fa; mn[u]=dis[u];
for (int i=1; i<=20; i++) f[u][i]=f[f[u][i-1]][i-1];
for (int i=head[u]; i; i=edge[i].nxt)
{
int v=edge[i].to;
dfs(v, u); mn[u]=min(mn[u], mn[v]);
}
}
void Kruskal()
{
sort(E+1, E+m+1, cmp);
for (int i=1; i<=n; i++) fa[i]=i; int now=n;
memset(head, 0, sizeof(head)); cnt=0;
for (int i=1; i<=m; i++)
{
int x=E[i].x, y=E[i].y, w=E[i].l;
int fx=find(x), fy=find(y);
if (fx^fy)
{
val[++now]=w; fa[fx]=fa[fy]=fa[now]=now;
add(now, fx, 1); add(now, fy, 1);
}
}
dfs(now, 0);
}
int main()
{
int T=read();
while (T--)
{
memset(head, 0, sizeof(head)); cnt=0;
memset(mn, 0, sizeof(mn));
memset(f, 0, sizeof(f));
n=read(); m=read();
for (int i=1; i<=m; i++)
{
int u=read(), v=read(), l=read(), a=read();
E[i]=(node){u, v, a}; add(u, v, l); add(v, u, l);
}
Dijkstra(); Kruskal();
int q=read(), k=read(), s=read();
for (int i=1, last=0; i<=q; i++)
{
int v=(read()+k*last-1)%n+1, p=(read()+k*last)%(s+1);
for (int j=20; ~j; j--)
if (f[v][j] && val[f[v][j]]>p) v=f[v][j];
printf("%d
", last=mn[v]);
}
}
return 0;
}
最后一题
此题借鉴了Dance_Of_Faith大佬的博客。
IOI2018 Werewolf狼人
Description
(N)个点(M)条边的无向连通图。对于每次询问,给定(S,E,L,R),求出能否从(S)出发,前一段只经过(L)到(N)的点,后一段只经过(1)到(R)的点,到达(E)点。
Solution
我们构建两棵Kruskal重构树。
一棵从大到小枚举点(u),枚举连边且编号比它大的(v),让(v)所在集合为(u)的儿子节点,那么从(S)出发前一段能经过的点在一个子树中,用树上倍增即可找到这个子树。
(E)能到达的后一段也可同理求得。
接下来要找到一个前后两段的分界线,我们只需考虑每一个点在两棵树的(dfs)序是否都在每一次询问的(S),(E)的出入的(dfs)序的区间中。这是经典的二维数点问题,离线用树状数组解决,在线用主席树解决。
因为本人比较懒,只给出了离线树状数组实现的代码。
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int N=200005;
struct node{int x, y, id, typ, sig;}q[N<<3];
bool operator < (node a, node b){return a.x<b.x || (a.x==b.x && a.typ<b.typ);}
vector<int> G[N];
int ans[N], d[N], n, m, Q;
struct dsu
{
int fa[N];
void init(){for (int i=1; i<=n; i++) fa[i]=i;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
}u, v;
struct Kruskal
{
vector<int> G[N];
int in[N], out[N], deg[N], dep[N], rnk[N], f[N][22], tot;
void add(int u, int v){G[u].push_back(v); deg[v]++;}
void dfs(int u, int fa)
{
in[u]=++tot; rnk[tot]=u;
dep[u]=dep[fa]+1; f[u][0]=fa;
for (int i=1; i<=20; i++) f[u][i]=f[f[u][i-1]][i-1];
for (int v: G[u]) dfs(v, u);
out[u]=tot;
}
void build()
{
int rt; for (int i=1; i<=n; i++) if (!deg[i]) rt=i;
dfs(rt, 0);
}
}U, V;
int getfa_U(int u, int anc)
{
for (int i=20; ~i; i--)
if (U.f[u][i] && U.f[u][i]>=anc) u=U.f[u][i];
return u;
}
int getfa_V(int v, int anc)
{
for (int i=20; ~i; i--)
if (V.f[v][i] && V.f[v][i]<=anc) v=V.f[v][i];
return v;
}
void add(int x){for (; x<=n; x+=x&-x) d[x]++;}
int query(int x){int res=0; for (; x; x-=x&-x) res+=d[x]; return res;}
inline int read()
{
int x=0,f=1;char ch=getchar();
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
return x*f;
}
int main()
{
n=read(); m=read(); Q=read();
for (int i=1; i<=m; i++)
{
int u=read()+1, v=read()+1;
G[u].push_back(v);
G[v].push_back(u);
}
u.init(); v.init();
for (int i=n; i; i--)
for (int j: G[i]) if (j>i)
{
int k=u.find(j);
if (i^k) U.add(i, k), u.fa[k]=u.fa[i];
}
for (int i=1; i<=n; i++)
for (int j: G[i]) if (j<i)
{
int k=v.find(j);
if (i^k) V.add(i, k), v.fa[k]=v.fa[i];
}
U.build(); V.build();
for (int i=1; i<=n; i++)
q[i].x=U.in[i], q[i].y=V.in[i], q[i].typ=0;
int cnt=n;
for (int i=1; i<=Q; i++)
{
int x=read()+1, y=read()+1, l=read()+1, r=read()+1;
int u=getfa_U(x, l), v=getfa_V(y, r);
q[++cnt]=(node){U.out[u], V.out[v], i, 1, 1};
if (U.in[u]>1)
q[++cnt]=(node){U.in[u]-1, V.out[v], i, 1, -1};
if (V.in[v]>1)
q[++cnt]=(node){U.out[u], V.in[v]-1, i, 1, -1};
if (U.in[u]>1 && V.in[v]>1)
q[++cnt]=(node){U.in[u]-1, V.in[v]-1, i, 1, 1};
}
sort(q+1, q+cnt+1);
for (int i=1; i<=cnt; i++)
if (q[i].typ)
ans[q[i].id]+=q[i].sig*query(q[i].y);
else add(q[i].y);
for (int i=1; i<=Q; i++) printf("%d
", (bool)ans[i]);
return 0;
}
写在最后
其实有很多题目能够用(Kruskal)重构树暴力优雅地写过,例如(NOIp2013)货车运输。所以看到求经过权值不超过(k)的点或边能到达的点的相关的信息时,就可以艰难简单地考虑用(Kruskal )重构树来做啦。