Kruskal 重構樹總結
Kruskal 重構樹
前言
聽了 @Wankupi 學長講了這個東西。
於是就爬過來學了。
確實是很有意思的東西。
不過貌似也很小眾,幾乎不咋用。
但是性質確實很優美。
特殊的題目也有奇效。
前置知識
-
Kruskal 演算法求解最小生成樹。
-
倍增
-
主席樹
至於為什麼需要這些玩意,其實並不必要
在題目裡會用到的。
定義
這個東西我找遍了各大詞條,並沒有一個合適的定義。
於是可以跳過。
實現過程
在執行 kruskal 的過程中,我們先將邊進行排序(排序的方式決定了重構樹的性質),之後遍歷每一條邊,檢視這條邊的兩個端點是否在同一個並查集之內。如果不在,那麼就新建一個節點 \(node\)
有一張圖畫的好啊!
圖片來源:@asd369
具體做法:
首先找到兩個端點在並查集中的根,之後檢查是否在一個並查集中。然後連邊就可以了。
namespace Kruskal{ inline void add(int u,int v){ nxt[++cnt]=head[u]; to[cnt]=v; head[u]=cnt; } struct node{ int u,v,dis; inline bool operator < (const node &a)const{ return dis<a.dis; } }e[maxm<<1]; int ff[maxn]; inline void init(){ for(re int i=1;i<maxn;i++){ ff[i]=i; } } int find(int x){ return x==ff[x]?x:ff[x]=find(ff[x]); } int val[maxn<<1],tot; inline void kruskal(){ sort(e+1,e+1+m); init(); for(re int i=1;i<=m;i++){ int x=find(e[i].u),y=find(e[i].v); if(x!=y){ val[++tot]=e[i].dis; ff[x]=ff[y]=ff[tot]=tot; add(tot,x);add(tot,y); fa[x][0]=fa[y][0]=tot; } } } }
性質
-
- Kruskal 重構樹是一棵樹(
這不是廢話?!
- Kruskal 重構樹是一棵樹(
而且他還是一棵二叉樹(雖然看上去也是廢話
還是一棵有根樹,根節點就是最後新建的節點。
-
- 若原圖不連通,那麼建出來的 Kruskal 重構樹就是一個森林。
-
- 如果一開始按照邊權升序排序,那麼建出的 Kruskal 重構樹就是一個大根堆,反之就是小根堆。
-
- 若一開始按照邊權升序排序,那麼
lca(u,v)
的權值代表了原圖中 \(u\) 到 \(v\) 路徑上最大邊權的最小值。反之就是最小邊權的最大值。
- 若一開始按照邊權升序排序,那麼
-
- Kruskal 重構樹中的葉子結點必定是原圖中的節點,其餘的節點都是原圖的一條邊。
-
- Kruskal
- Kruskal
一條一條來看:
對於性質 \(1\) 和 \(2\),比較顯然,我們就不說了。
對於性質 \(3\) 和 \(4\),由於一開始對邊權升序排序,所以我們首先遍歷到的邊一定是權值最小的。
於是對於 Kruskal 重構樹中的某一個節點,它的子樹中任意一個節點的權值一定小於它本身。
那麼可以知道,權值越小的深度越大,權值越大的深度越小。
於是這是大根堆性質。
有了大根堆性質,我們可以發現,由於邊權升序,其實就是求解最小生成樹的過程,於是能出現在 Kruskal 重構樹中的節點必然是要滿足也出現在原圖的最小生成樹中的,那麼在找 LCA
的過程中,找到的必然是在 Kruskal 重構樹上這條路徑中深度最小的點,也就是權值最大的。對於原圖來說,這個權值最大的恰好是從 \(u\) 到 \(v\) 最小值。
若一個點能通過一條路徑到達,那麼我們走最小生成樹上的邊也一定能到達該節點。
於是滿足了最大值最小的性質。
同理降序也能夠得出最小值最大的性質。
對於性質 \(5\),可以畫圖解決。
對於性質 \(6\),可以發現,建出 Kruskal 重構樹的過程其實也就是求解最小生成樹的過程,那麼 Kruskal 重構樹中新增加的節點數也就是最小生成樹中的邊數。而最小生成樹中的邊數最多是 \(n-1\) 條,於是 Kruskal 重構樹中新增加的節點數也就是 \(n-1\) 個。
應用
根據上面的性質們,Kruskal 重構樹有幾種常見用法:
u->v路徑上的最大值最小 or u->v路徑上的最小值最大
這就是上面的性質 \(3\) 和 \(4\) 了。
於是直接套板子就行了。
也給我們一個提示,遇到這種最大值最小或者最小值最大這種類似的語句,可以不急著想二分,還可以想想 Kruskal 重構樹。
例題就是 P1967 [NOIP2013 提高組] 貨車運輸
求解路徑上最小值最大。
將邊降序排序,建出 Kruskal 重構樹,注意處理一下有可能是個森林。
lca
怎麼搞都行,不過我喜歡樹剖,比較優雅。
查詢在 Kruskal 重構樹中 lca(u,v)
的權值就好了。
//#define LawrenceSivan
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define re register
const int maxn=1e5+5;
#define INF 0x3f3f3f3f
int n,m,tot,q;
struct node{
int u,v,dis;
inline bool operator < (const node &a)const{
return dis>a.dis;
}
}a[maxn<<1];
int head[maxn],to[maxn<<1],nxt[maxn<<1],cnt;
int val[maxn<<1];
inline void add(int u,int v){
to[++cnt]=v;
nxt[cnt]=head[u];
head[u]=cnt;
}
int dep[maxn],size[maxn],fa[maxn],son[maxn],top[maxn];
bool vis[maxn];
void dfs1(int u,int f){
size[u]=1;
vis[u]=true;
fa[u]=f;
dep[u]=dep[f]+1;
for(re int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==f)continue;
dfs1(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]])son[u]=v;
}
}
void dfs2(int u,int topf){
top[u]=topf;
if(!son[u])return;
dfs2(son[u],topf);
for(re int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==fa[u]||v==son[u])continue;
dfs2(v,v);
}
}
inline int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
int ff[maxn];
int find(int x){
return x==ff[x]?x:ff[x]=find(ff[x]);
}
inline void init(){
for(re int i=1;i<maxn;i++){
ff[i]=i;
}
}
inline void Kruskal(){
sort(a+1,a+1+m);
init();
for(re int i=1;i<=m;i++){
int x=find(a[i].u),y=find(a[i].v);
if(x!=y){
val[++tot]=a[i].dis;
ff[tot]=ff[x]=ff[y]=tot;
add(tot,x);
add(tot,y);
}
}
for(re int i=1;i<=tot;i++){
if(!vis[i]){
int f=find(i);
dfs1(f,0);
dfs2(f,f);
}
}
}
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}
int main(){
#ifdef LawrenceSivan
freopen("aa.in","r",stdin);
freopen("aa.out","w",stdout);
#endif
n=read();m=read();tot=n;
for(re int i=1;i<=m;i++){
a[i].u=read();a[i].v=read();a[i].dis=read();
}
Kruskal();
q=read();
while(q--){
int u=read(),v=read();
if(find(u)!=find(v))puts("-1");
else printf("%d\n",val[lca(u,v)]);
}
return 0;
}
從 u 出發只經過邊權不超過 x 的邊能到達的節點
根據性質 \(3\),可以發現,只需要找到邊權升序的 Kruskal 重構樹中找到深度最小的,點權不超過 \(x\) 的節點,那麼這個節點的子樹即為所求。
找這個點一般用樹上倍增
我不要!!!!樹剖黨聲嘶力竭
沒辦法這玩意還是倍增好
我們考慮當前我們找到的這個節點為 \(x\),然後我們倍增列舉它的祖先,由於是升序排序,所以它祖先的點的點權必然大於等於它的點權,於是,我們倍增的時候只要判斷如果它的祖先的點權就好了。
inline void kruskal(){
sort(e+1,e+1+m);
init();
for(re int i=1;i<=m;i++){
int x=find(e[i].u),y=find(e[i].v);
if(x!=y){
val[++tot]=e[i].dis;
ff[x]=ff[y]=ff[tot]=tot;
add(tot,x);add(tot,y);
fa[x][0]=fa[y][0]=tot;
}
}
dfs(tot);
}
namespace BIN{
int fa[maxn<<1][21],range[maxn<<1][2];
void dfs(int u){
for(re int i=1;i<=20;i++){
fa[u][i]=fa[fa[u][i-1]][i-1];
}
...
}
}
int main(){
while(q--){
int u=read(),x=read();
for(re int i=20;~i;i--){
if(fa[u][i]&&val[fa[u][i]]<=x)v=fa[u][i];
}
}
}
大概就是這樣的。
例題:P4197 Peaks
這個題其實也能用線段樹合併做。
其實這個題在題單裡躺了好久了,本來是打算線段樹合併做的,然後學了重構樹於是就用重構樹了。
主體思路是裸的,多出來的就是一個第 \(k\) 大。
這就是為啥我說需要主席樹當做前置知識
然後子樹區間第 \(k\) 大,dfs 序 + 主席樹大力維護就行了。
碼農題,不好,思維題,好!
但是思維題不會做嚶嚶嚶
其實還是有很多細節問題的。
首先問題就是關於無解情況的判斷。
肯定是對於一個滿足條件的子樹,子樹中節點個數不足 \(k\) 個。
需要注意的是,由於 Kruskal 重構樹的性質 \(5\),我們知道在 Kruskal 重構樹種只有葉子節點才是會對答案產生貢獻的,於是我們需要統計的子樹大小並不是我們以往統計的那樣,而是隻統計葉子節點。
實現也很簡單:
void dfs(int u){
for(re int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==fa[u][0])continue;
fa[v][0]=u;
dfs(v);
size[u]+=size[v];
}
if(!size[u])size[u]=1;
}
剩下的部分其實就好說很多了。
注意一下離散化就行了。
//#define LawrenceSivan
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define re register
const int maxn=2e5+5;
const int maxm=5e5+5;
#define INF 0x3f3f3f3f
int n,m,q,tmp,num;
int a[maxn],b[maxn];
int head[maxn<<1],to[maxn<<1],nxt[maxn<<1],cnt;
namespace SegmentTree{
inline void Discretization(){
sort(b+1,b+1+n);
tmp=unique(b+1,b+1+n)-b-1;
for(re int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tmp,a[i])-b;
}
struct SegmentTree{
int lc,rc,v;
#define ls(x) st[x].lc
#define rs(x) st[x].rc
}st[maxn<<6];
int segtot,root[maxn<<1];
void build(int &rt,int l,int r){
rt=++segtot;
if(l==r)return;
int mid=(l+r)>>1;
build(ls(rt),l,mid);
build(rs(rt),mid+1,r);
}
int modify(int rt,int l,int r,int x){
int t=++segtot;
ls(t)=ls(rt),rs(t)=rs(rt);
st[t].v=st[rt].v+1;
if(l==r)return t;
int mid=(l+r)>>1;
if(x<=mid)ls(t)=modify(ls(t),l,mid,x);
else rs(t)=modify(rs(t),mid+1,r,x);
return t;
}
int query(int x,int y,int l,int r,int k){
int xx=st[rs(y)].v-st[rs(x)].v;
if(l==r)return l;
int mid=(l+r)>>1;
if(k<=xx)return query(rs(x),rs(y),mid+1,r,k);
else return query(ls(x),ls(y),l,mid,k-xx);
}
}
using namespace SegmentTree;
namespace BIN{
int fa[maxn<<1][30],pos[maxn<<1],st1[maxn<<1],ed[maxn<<1],size[maxn<<1];
void dfs(int u){
pos[++num]=u;st1[u]=num;
for(re int i=1;i<=25;i++){
fa[u][i]=fa[fa[u][i-1]][i-1];
}
for(re int i=head[u];i;i=nxt[i]){
int v=to[i];
if(v==fa[u][0])continue;
fa[v][0]=u;
dfs(v);
size[u]+=size[v];
}
if(!size[u])size[u]=1;
ed[u]=num;
}
}
using namespace BIN;
namespace Kruskal{
inline void add(int u,int v){
nxt[++cnt]=head[u];
to[cnt]=v;
head[u]=cnt;
}
struct node{
int u,v,dis;
inline bool operator < (const node &a)const{
return dis<a.dis;
}
}e[maxm];
int ff[maxn<<1];
inline void init(){
for(re int i=1;i<maxn;i++){
ff[i]=i;
}
}
int find(int x){
return x==ff[x]?x:ff[x]=find(ff[x]);
}
int val[maxn<<1],tot;
inline void kruskal(){
sort(e+1,e+1+m);
init();
for(re int i=1;i<=m;i++){
int x=find(e[i].u),y=find(e[i].v);
if(x!=y){
val[++tot]=e[i].dis;
ff[x]=ff[y]=ff[tot]=tot;
add(tot,x);add(tot,y);
}
}
dfs(tot);
}
}
using namespace Kruskal;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}
int main(){
#ifdef LawrenceSivan
freopen("aa.in","r",stdin);
freopen("aa.out","w",stdout);
#endif
n=read();m=read();q=read();tot=n;
for(re int i=1;i<=n;i++){
a[i]=b[i]=read();
}
Discretization();
for(re int i=1;i<=m;i++){
e[i].u=read();
e[i].v=read();
e[i].dis=read();
}
kruskal();
for(re int i=1;i<=tot;i++){
root[i]=root[i-1];
if(pos[i]<=n)root[i]=modify(root[i-1],1,tmp,a[pos[i]]);
}
while(q--){
int v=read(),x=read(),k=read();
for(re int i=25;~i;i--){
if(fa[v][i]&&val[fa[v][i]]<=x)v=fa[v][i];
}
if(size[v]<k){
puts("-1");
continue;
}
else printf("%d\n",b[query(root[st1[v]-1],root[ed[v]],1,tmp,k)]);
}
return 0;
}
看了題解以後發現這題也可以不使用 dfs 序,由於每個節點直接對應一個區間,所以可以直接處理。
注意區間是左開右閉的。
//#define LawrenceSivan
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define re register
const int maxn=1e5+5;
const int maxm=5e5+5;
#define INF 0x3f3f3f3f
int n,m,q,tmp,num;
int a[maxn],b[maxn];
int head[maxn<<1],to[maxm<<1],nxt[maxm<<1],cnt;
namespace SegmentTree{
inline void Discretization(){
sort(b+1,b+1+n);
tmp=unique(b+1,b+1+n)-b-1;
for(re int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tmp,a[i])-b;
}
struct SegmentTree{
int lc,rc,v;
#define ls(x) st[x].lc
#define rs(x) st[x].rc
}st[maxn<<5];
int segtot,root[maxn];
void build(int &rt,int l,int r){
rt=++segtot;
if(l==r)return;
int mid=(l+r)>>1;
build(ls(rt),l,mid);
build(rs(rt),mid+1,r);
}
int modify(int rt,int l,int r,int x){
int t=++segtot;
ls(t)=ls(rt),rs(t)=rs(rt);
st[t].v=st[rt].v+1;
if(l==r)return t;
int mid=(l+r)>>1;
if(x<=mid)ls(t)=modify(ls(t),l,mid,x);
else rs(t)=modify(rs(t),mid+1,r,x);
return t;
}
int query(int x,int y,int l,int r,int k){
int xx=st[rs(y)].v-st[rs(x)].v;
if(l==r)return l;
int mid=(l+r)>>1;
if(k<=xx)return query(rs(x),rs(y),mid+1,r,k);
else return query(ls(x),ls(y),l,mid,k-xx);
}
}
using namespace SegmentTree;
namespace BIN{
int fa[maxn<<1][21],range[maxn<<1][2];
void dfs(int u){
for(re int i=1;i<=20;i++){
fa[u][i]=fa[fa[u][i-1]][i-1];
}
range[u][0]=num;
if(!head[u]){
range[u][1]=++num;
root[num]=modify(root[num-1],1,tmp,a[u]);
return;
}
for(re int i=head[u];i;i=nxt[i]){
int v=to[i];
dfs(v);
}
range[u][1]=num;
}
}
using namespace BIN;
namespace Kruskal{
inline void add(int u,int v){
nxt[++cnt]=head[u];
to[cnt]=v;
head[u]=cnt;
}
struct node{
int u,v,dis;
inline bool operator < (const node &a)const{
return dis<a.dis;
}
}e[maxm<<1];
int ff[maxn];
inline void init(){
for(re int i=1;i<maxn;i++){
ff[i]=i;
}
}
int find(int x){
return x==ff[x]?x:ff[x]=find(ff[x]);
}
int val[maxn<<1],tot;
inline void kruskal(){
sort(e+1,e+1+m);
init();
for(re int i=1;i<=m;i++){
int x=find(e[i].u),y=find(e[i].v);
if(x!=y){
val[++tot]=e[i].dis;
ff[x]=ff[y]=ff[tot]=tot;
add(tot,x);add(tot,y);
fa[x][0]=fa[y][0]=tot;
}
}
build(root[0],1,tmp);
dfs(tot);
}
}
using namespace Kruskal;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}
int main(){
#ifdef LawrenceSivan
freopen("aa.in","r",stdin);
freopen("aa.out","w",stdout);
#endif
n=read();m=read();q=read();tot=n;
for(re int i=1;i<=n;i++){
a[i]=b[i]=read();
}
Discretization();
for(re int i=1;i<=m;i++){
e[i].u=read();
e[i].v=read();
e[i].dis=read();
}
kruskal();
while(q--){
int v=read(),x=read(),k=read();
for(re int i=20;~i;i--){
if(fa[v][i]&&val[fa[v][i]]<=x)v=fa[v][i];
}
if(range[v][1]-range[v][0]<k){
puts("-1");
continue;
}
else printf("%d\n",b[query(root[range[v][0]],root[range[v][1]],1,tmp,k)]);
}
return 0;
}