1. 程式人生 > 其它 >演算法學習筆記之kruskal重構樹

演算法學習筆記之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;
}