演算法學習筆記之kruskal重構樹
前言
作為OIer中的精英,相信各位在初學時都學過簡單(毒瘤)的kruskal最小生成樹演算法。
它可以在O(nlogn)的時間複雜度中構造出一顆最小生成樹,在保證連通性的情況下使得邊權和最小。
然而,我們還有一種對其的使用方法:
當我們把邊排好順序,從小到大一次列舉,合併兩個節點就建一個新節點作為他們的父親,節點權值為邊的權值。
這樣建出的樹具有優良的性質,每兩個點的LCA的權值為他們路徑上最小邊的最大權值,而葉子節點為原節點,非葉子節點為新建的節點。
運用它的優良性質,我們就可以將路徑最值迎刃而解了!
例題
1.[NOIP2013 提高組] 貨車運輸
一道非常經典的生成樹問題,有多種解法。
1.最大生成樹上跳LCA求最值:
先把最大生成樹建出,然後在找兩個點LCA時維護最小值,最後輸出。時間複雜度O(nlogn);
程式碼:
點選檢視程式碼
#include<bits/stdc++.h> #define in read() using namespace std; const int N=1e5+100; const int M=5e5+100; int n,m,q; int fa[N]; int find(int x){ if(fa[x]==x){ return x; } return fa[x]=find(fa[x]); } struct edge{ int x,y,val; }e[M]; bool cmp(edge a,edge b){ return a.val>b.val; } int head[N],to[M],nxt[M],w[M],cnt; void add(int u,int v,int z){ nxt[++cnt]=head[u]; head[u]=cnt; w[cnt]=z; to[cnt]=v; } int f[N][30]; int minn[N][30]; int dep[N],vis[N]; void pre(int u,int father){ for(int i=1;i<=20;i++){ f[u][i]=f[f[u][i-1]][i-1]; minn[u][i]=min(minn[u][i-1],minn[f[u][i-1]][i-1]); } vis[u]=1; for(int i=head[u];i;i=nxt[i]){ int v=to[i]; if(v==father||vis[v]) continue; dep[v]=dep[u]+1; minn[v][0]=w[i]; f[v][0]=u; pre(v,u); } } inline int LCA(int x,int y){ int ans=0x3f3f3f3f; if(dep[x]<dep[y]){ swap(x,y); } for(int i=20;i>=0;i--){ if(dep[f[x][i]]>=dep[y]){ ans=min(ans,minn[x][i]); x=f[x][i]; } if(x==y){ return ans; } } for(int i=20;i>=0;i--){ if(f[x][i]!=f[y][i]){ ans=min(ans,min(minn[y][i],minn[x][i])); x=f[x][i]; y=f[y][i]; } } ans=min(ans,min(minn[x][0],minn[y][0])); return ans; } inline int read(){ static char ch; int res=0; while((ch=getchar())<'0'||ch>'9'); res=ch-'0'; while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-'0'; return res; } inline void kruskal(){ for(int i=1;i<=n;i++){ fa[i]=i; } sort(e+1,e+m+1,cmp); for(int i=1;i<=m;i++){ int x=find(e[i].x); int y=find(e[i].y); if(x!=y){ fa[x]=y; add(e[i].x,e[i].y,e[i].val); add(e[i].y,e[i].x,e[i].val); } } return ; } int main(){ n=in,m=in; for(int i=1;i<=m;i++){ e[i].x=in,e[i].y=in,e[i].val=in; } kruskal(); for(int i=1;i<=n;i++){ if(!vis[i]){ dep[i]=1; f[i][0]=i; minn[i][0]=0x3f3f3f3f; pre(i,i); } } q=in; for(int i=1;i<=q;i++){ int x,y; x=in,y=in; if(find(x)!=find(y)){ cout<<-1<<endl; continue; } int lca=LCA(x,y); cout<<lca<<endl; } return 0; }
2.kruskal重構樹解法
結合上面提到的重構樹的優良性質,我們只需要把邊從大到小放入重構樹中。這樣生成的重構樹滿足兩點之間的LCA的權值為路徑上最小值的最大。也就滿足此題貨物運輸的要求。
點選檢視程式碼
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; struct edge{ int u, v, w; bool operator < (const edge &x){ return w > x.w; //從大到小排序 } }e[N]; vector<int> g[N]; //vector存圖 int n, m, q, cnt, fa[N], vis[N], val[N], d[N], f[N][30]; int find(int x){ return x == fa[x] ? x : fa[x] = find(fa[x]); } //並查集維護祖先 void kruskal(){ sort(e + 1, e + m + 1); for(int i = 1; i <= n; i++) fa[i] = i; for(int i = 1; i <= m; i++){ int fu = find(e[i].u), fv = find(e[i].v); if(fu != fv){ val[++cnt] = e[i].w; fa[cnt] = fa[fu] = fa[fv] = cnt; g[cnt].push_back(fu); g[cnt].push_back(fv); g[fu].push_back(cnt); g[fv].push_back(cnt); } } } void dfs(int u, int fa){ d[u] = d[fa] + 1, f[u][0] = fa, vis[u] = 1; for(int i = 1; (1 << i) <= d[u]; i++) //倍增陣列維護 f[u][i] = f[f[u][i-1]][i-1]; for(int i = 0; i < g[u].size(); i++){ int v = g[u][i]; if (v != fa) dfs(v, u); } } int lca(int x, int y){ if (d[x] < d[y]) swap(x, y); int k = d[x] - d[y]; for(int i = 25; i >= 0; i--) if((1 << i) & k) x = f[x][i]; if(x == y) return x; for(int i = 25; i >= 0; i--) if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; return f[x][0]; } int main(){ // freopen("truck.in", "r", stdin); // freopen("truck.out", "w", stdout); cin >> n >> m; cnt = n; for(int i = 1; i <= m; i++) cin >> e[i].u >> e[i].v >> e[i].w; kruskal(); for(int i = 1; i <= cnt; i++){ if(!vis[i]){ int f = find(i); dfs(f, 0); } } cin >> q; while(q--){ int u, v; cin >> u >> v; if (find(u) != find(v)) cout << -1 << endl; else cout << val[lca(u, v)] << endl; } return 0; }
2.BZOJ3732 Network
解法:
同是kruskal重構樹的模板題,與上一題相似。
程式碼:
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const int N=1e5;
const int M=6e5+300;
struct edge{
int u,v,w;
}e[M];
int ch[N][2],fa[N],f[N][30],val[N],cnt,dep[N];
int n,m,k;
bool cmp(edge x,edge y){
return x.w<y.w;
}
int find(int x){
if(fa[x]==x) return fa[x];
else return fa[x]=find(fa[x]);
}
void Kruskal(){
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++){
int x=e[i].u;
int y=e[i].v;
int fx=find(x);
int fy=find(y);
if(fx==fy) continue;
ch[++cnt][0]=fx,ch[cnt][1]=fy;
fa[fa[x]]=fa[fa[y]]=f[fa[x]][0]=f[fa[y]][0]=cnt;
fa[x]=fa[y]=cnt;
val[cnt]=e[i].w;
}
}
void dfs(int pos){
if(!ch[pos][0]&&!ch[pos][1]){
return ;
}
dep[ch[pos][1]]=dep[ch[pos][0]]=dep[pos]+1;
dfs(ch[pos][0]);
dfs(ch[pos][1]);
}
int LCA(int x,int y){
if(dep[x]<dep[y]){
swap(x,y);
}
for(int i=20;i>=0;i--){
if(dep[f[x][i]]>=dep[y]){
x=f[x][i];
}
}
if(x==y){
return y;
}
for(int i=20;i>=0;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main(){
cin>>n>>m>>k;
cnt=n;
for(int i=1;i<=2*n;i++){
fa[i]=i;
}
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].w;
}
Kruskal();
dfs(cnt);
for(int i=1;i<=20;i++){
for(int j=1;j<=2*n;j++){
f[j][i]=f[f[j][i-1]][i-1];
}
}
for(int i=1;i<=k;i++){
int x,y;
cin>>x>>y;
cout<<val[LCA(x,y)]<<endl;
}
return 0;
}
3.[NOI2018] 歸程
哦~~~就是這道題!他讓SPFA的時代終結了!Dij的時代到來了!
思路如下:
考慮每一條邊的海拔高度,先從大到小排序,然後kruskal構建一顆重構樹。
在這顆重構樹上,每一個新建的子節點的權值是它的子樹中海拔最低的邊的海拔。
只要降雨量小於其權值那麼它子樹中的節點都可以乘車到到達,對路程的奉獻為0。
我們就從出發點不斷倍增,找到海拔剛好小於積水線的點,然後返回它的子樹中到原點的最小距離。因為其子樹中的所有點都可以開車到達。
最小距離一開始就預處理好(注意SPFA),在重構的時候合併最小值。
程式碼:
點選檢視程式碼
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define re register
using namespace std;
inline int read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x;
}
const int N=4e5+10;
int T,n,m,last;
struct node{
int u,v,l,a,nxt;
bool operator < (const node &b) const{
return a>b.a;
}
}e[N<<1],tmp[N<<1],edge[N<<1];
int head[N],tot,dis[N];
struct heap{
int x,dis;
bool operator < (const heap &b) const{return dis>b.dis;}
};
int f[N],cnt;
inline void Add(int x,int y,int z){
edge[++tot].v=y,edge[tot].l=z,edge[tot].nxt=head[x];
head[x]=tot;
}
inline void dijkstar(){
priority_queue <heap> q;
memset(dis,0x3f,sizeof dis);
dis[1]=0;
q.push((heap){1,0});
while(!q.empty()){
heap now=q.top();
q.pop();
int x=now.x;
if(dis[x]<now.dis) continue;
for(re int i=head[x];i;i=edge[i].nxt){
int y=edge[i].v;
if(dis[y]>dis[x]+edge[i].l){
dis[y]=dis[x]+edge[i].l;
q.push((heap){y,dis[y]});
}
}
}
}
inline int find(int x){
return f[x]==x?x:f[x]=find(f[x]);
}
inline void add(int x,int y){
edge[++tot].v=y,edge[tot].nxt=head[x];
head[x]=tot;
}
inline void kruskal(){
sort(e+1,e+m+1);
for(re int i=1;i<=n;i++){
f[i]=i;
}
cnt=n;
int num=0;
for(re int i=1;i<=m;i++){
int fu=find(e[i].u),fv=find(e[i].v);
if(fu!=fv){
num++;
tmp[++cnt].a=e[i].a;
f[fu]=f[fv]=f[cnt]=cnt;
add(cnt,fu),add(cnt,fv);
}
if(num==n-1) break;
}
}
int fa[N][20],dep[N];
inline void dfs(int x,int p){
dep[x]=dep[p]+1;
fa[x][0]=p;
for(re int i=1;i<=19;i++){
fa[x][i]=fa[fa[x][i-1]][i-1];
}
for(re int i=head[x];i;i=edge[i].nxt){
int y=edge[i].v;
dfs(y,x);
tmp[x].l=min(tmp[x].l,tmp[y].l);
}
}
inline int query(int x,int y){
for(re int i=19;i>=0;i--){
if(dep[x]-(1<<i)>0&&tmp[fa[x][i]].a>y){
x=fa[x][i];
}
}
return tmp[x].l;
}
inline void solve(){
kruskal();
dfs(cnt,0);
int q=read(),k=read(),s=read();
while(q--){
int x=(k*last+read()-1)%n+1,y=(k*last+read())%(s+1);
printf("%d\n",last=query(x,y));
}
}
inline void init(){
memset(head,0,sizeof head);
memset(fa,0,sizeof fa);
memset(f,0,sizeof f);
memset(tmp,0,sizeof tmp);
memset(edge,0,sizeof edge);
last=tot=0;
}
int main(){
T=read();
while(T--){
init();
n=read(),m=read();
for(int i=1;i<=m;i++){
e[i].u=read(),e[i].v=read(),e[i].l=read(),e[i].a=read();
Add(e[i].u,e[i].v,e[i].l);
Add(e[i].v,e[i].u,e[i].l);
}
dijkstar();
for(re int i=1;i<=n;i++) tmp[i].l=dis[i];
for(re int i=n+1;i<=n<<1;i++) tmp[i].l=INF;
memset(head,0,sizeof head),tot=0;
solve();
}
return 0;
}
4.P4197 Peaks
蒟蒻打了一天調了一天還是沒有調出來……
本題思路是kruskal+主席樹求區間第k大。
先把邊從小到大排序,然後重構。
當在dfs預處理時,每一次掃到一個葉子節點就加入主席樹中,注意要先離散化。
同時記錄每一個節點最左端的葉子節點編號,以及最右端的葉子節點編號,方便主席樹查詢。
在每一次詢問時,先往上倍增找到所有能走到的山峰集合,然後再利用主席樹查詢k大值。
程式碼:
點選檢視程式碼
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
const int M=5e5+10;
struct Node{
int from,to,val;
}g[M];
struct Edge{
int to,next;
}f[M<<1];
int n,m,q,fa[N],h[N][25],v[N],cnt,tot,head[N],rt[N<<5],ls[N<<5],rs[N<<5],a[N],b[N],sz,num,range[N][2],sum[N<<5];
bool cmp(Node a,Node b){
return a.val<b.val;
}
int find(int a){
if(fa[a]==a) return a;
else return fa[a]=find(fa[a]);
}
void add(int u,int v){
f[++cnt].to=v;
f[cnt].next=head[u];
head[u]=cnt;
}
//重構
void kruskal()
{
for (int i = 1; i <= 2 * n; i ++) fa[i] = i;
sort(g + 1,g + 1 + m,cmp);
tot = n;
for (int i = 1; i <= n; i ++)
{
int x = find(g[i].from),y = find(g[i].to);
if (x == y) continue;
fa[x] = fa[y] = ++ tot,v[tot] = g[i].val;
add(tot,x),add(tot,y);
h[x][0] = h[y][0] = tot;
}
}
//主席樹
void build(int &x,int l,int r)
{
x = ++ cnt;
if (l == r) return;
int mid = l + r >> 1;
build(ls[x],l,mid),build(rs[x],mid + 1,r);
}
void modify(int pre,int &rt,int l,int r,int k)
{
rt = ++ cnt;
sum[rt] = sum[pre] + 1;
if (l == r) return;
int mid = l + r >> 1;
if (k <= mid) rs[rt] = rs[pre],modify(ls[pre],ls[rt],l,mid,k); else ls[rt] = ls[pre],modify(rs[pre],rs[rt],mid + 1,r,k);
}
int query(int x,int y,int l,int r,int k)
{
if (l == r) return l;
int mid = l + r >> 1,q = sum[rs[y]] - sum[rs[x]];
if (k <= q) return query(rs[x],rs[y],mid + 1,r,k); else return query(ls[x],ls[y],l,mid,k - q);
}
//維護資訊
void dfs(int u)
{
for (int i = 1; i <= 20; i ++) h[u][i] = h[h[u][i - 1]][i - 1];
range[u][0] = num;
if (!head[u])
{
int x = lower_bound(b + 1,b + 1 + sz,a[u]) - b;
range[u][0] = ++ num;
modify(rt[num - 1],rt[num],1,sz,x);
return;
}
for (int i = head[u]; i; i = f[i].next)
dfs(f[i].to);
range[u][1] = num;
}
int read()
{
int x = 0,w = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') w = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0'; ch = getchar();}
return x * w;
}
signed main(){
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+n+1);
sz=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=m;i++){
cin>>g[i].from>>g[i].to;
cin>>g[i].val;
}
kruskal();
cnt=0;
build(rt[0],1,sz);
dfs(tot);
int lans=0;
while(q--){
int x,y,z;
cin>>x>>y>>z;
// cout<<x<<' '<<y<<' '<<z<<endl;
for(int i=20;i>=0;i--){
if(h[x][i]&&v[h[x][i]]<=y) x=h[x][i];
}
if(range[x][1]-range[x][0]<z) {
printf("%lld\n",-1ll);
lans=0;
continue;
}
else {
printf("%lld\n",lans=b[query(rt[range[x][0]],rt[range[x][1]],1,sz,z)]);
}
// cout<<lans<<endl;
}
return 0;
}