最小生成樹

Abnormal123發表於2024-11-13

最小生成樹

模板題:【模板】最小生成樹

求最小生成樹的邊權和。

Prim

這似乎是我最早學的最小生成樹演算法。也是忘的最早的

首先注意到,由 \(n\) 個節點和 $ n-1 $ 條邊構成的 連通圖 一定是樹。那麼只需要選 \(n-1\) 條邊使圖連通,求最小代價。不難發現只要保證結果不出現環就可能是答案。

考慮貪心求解,對於當前生成樹構成的連通塊,每次向外擴充代價最小的邊,併合並對應的節點即可。注意,如果兩個節點都在連通塊內,那麼它們之間不能再加邊,否則一定會出現環。

暴力做的話可以做到 $ O(n^2+m) $ ,用單調佇列維護可以做到 $ O( (n+m)\log n) $ ,具體實現類似於dijkstra

CODE
struct node{
	int x;ll y;
	bool operator<(node an)const{
		return y>an.y;
	}
};
priority_queue<node>q;
void prim(){
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;
	q.push({1,0});
	while(!q.empty()){
		node u=q.top();q.pop();
		if(vis[u.x])continue;
		vis[u.x]=1;
		ans+=u.y;
		for(int i=head[u.x];i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[v]>e[i].val){
				dis[v]=e[i].val;
				q.push({v,e[i].val});
			}
		}
	}
}

Kruskal

一種更常用的演算法。

由於只要保證生成樹上沒有環,在此基礎上求最小代價和。將所有邊按邊權升序排序,依次遍歷,如果兩點不在同一連通塊就合併,並查集維護。複雜度 $ O(m\log m) $ 。可以發現這就是最優答案。

CODE
struct edge{
	int x,y,val;
}e[N];
bool cmp(edge fir,edge sec){
	return fir.val<sec.val;
}
int find(int x){
	if(x==fa[x])return x;
	else return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
	int fx=find(x),fy=find(y);
	fa[fx]=fy;
}
void kruskal(){
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=m;i++){
		if(find(e[i].x)!=find(e[i].y)){
			ans+=e[i].val;
			merge(e[i].x,e[i].y);
		}
	}
}

Boruvka

最近剛學的菠蘿演算法。

初始時,我們把每個節點看做一個連通塊,進行如下操作:

  1. 對於每個連通塊,尋找連線它和另一個不同的連通塊的最小邊

  2. 將最小邊連線的兩個連通塊合併,並統計答案(這裡每次合併前都要判斷是否屬於同一連通塊)

  3. 如果所有節點都合併到同一連通塊,退出

有點像前兩種演算法結合起來。正確性是顯然的,因為每次連通塊向外連邊一定是當前最優也是以後最優的。每次合併連通塊數量至少減半,複雜度為 $ O(m\log n) $

CODE
struct node{
	int x,y;
}minn[N];
int find(int x){
	if(x==fa[x])return x;
	else return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){
	int fx=find(x),fy=find(y);
	fa[fx]=fy;
}
bool check(){
	for(int i=2;i<=n;i++){
		if(find(i)!=find(i-1))return 1;
	}
	return 0;
}
void Boruvka(){
	while(check()){
		for(int i=1;i<=n;i++)minn[i].y=inf;
		for(int i=1;i<=n;i++){
			int fx=find(i);
			for(int j=head[i];j;j=e[j].nxt){
				if(fx!=find(e[j].to)&&e[j].val<minn[fx].y)minn[fx]={e[j].to,e[j].val};
			}
		}
		for(int i=1;i<=n;i++){
			int fx=find(i);
			if(fx!=find(minn[fx].x)){
				merge(fx,minn[fx].x);
				ans+=minn[fx].y;
			}
		}
	}
}

Boruvka 在實現難度和常數方面不如前兩種演算法,但是在某一類題目中有明顯優勢:給定 \(n\) 個點,每個點都有權值,並且兩兩之間可以任意連邊,邊權以某種方式定義且與兩個端點的權值有關,求最小生成樹的邊權和。可以參考練習題。

練習題

限速

題意

給定 \(n\) 個點, \(m\) 條帶權邊的連通圖,首先選擇 $ n-1 $ 條邊構成一棵樹,其次,可以操作一次使一條邊權加1或減1,使的生成樹的最大邊權 恰好\(k\) ,求最小操作次數。 $ n,m \le 2 \times 10^5 ,k\le 1\times 10^9 $

題解

為敘述方便,我們定義權值不超過 \(k\) 的邊為A邊,其餘為B邊,考慮選擇的邊有兩種情況:

  1. 只有A邊

  2. 存在B邊

第一種情況代價最小的方案是把邊權最大的A邊加到 \(k\) ;第二種情況最小代價是把B邊都減至 \(k\)

先判斷只有A邊能否構成生成樹,如果可以,一定能選擇邊權最大的A邊作為候選答案。考慮加入一條B邊,在形成的生成樹一定會出現一個環,斷掉環上任意一條A邊即可。 $ ans = \min \{ k- \max a_i \ , \min b_i -k \} $

如果A邊不能構成生成樹,直接跑最小生成樹使最大權最小。

排列最小生成樹 (pmst)

題意

給定一個 \(1\)\(n\) 的排列 \(p\) 。構造一個有 \(n\) 個節點的無向完全圖,定義節點 \(i\) 和節點 \(j\) 的邊權為 $ | p_i-p_j | \times | i-j | $ ,求最小生成樹的邊權和。$ n\le 5 \times 10^4 $

題解

發現神秘結論即可

首先注意到答案不會很大,因為一定有這樣的一種構造:生成樹是一條編號連續的鏈,這樣每條邊的權值都不會超過 $ n $ 。再考慮正解,發現每個點只有 $ O(\sqrt N) $ 條邊是有意義的,其它邊權都超過 \(n\) 了。所以只要先按 \(i\) 排序,取相鄰的 $ \sqrt n $ 個點建邊,再按 $ p_i $ 排序,重複操作,跑 kruskal 。排序的時候建議桶排, sort 和不優良的實現都會T飛的。

星際聯邦

題意

一個 $ n $ 個節點的完全圖,第 $ i $ 個節點的權值為 $ u_i $ (可能為負),從 $ i $ 向 $ j $ 的邊權為 $ u_j-u_i $ (要求 $ i < j $ ),求最小生成樹邊權和。 $ n\le 3 \times 10^5 $

題解

考慮 Boruvka 怎麼做,難點在於 $ O(n) $ 的複雜度求出每個連通塊向外的最小邊。發現對於節點 $ i $ ,與其不屬於同一連通塊且邊權最小的點只可能有兩個 : 向前最小的 $ u_j ( j < i ,f_j \neq f_i ) $ 和向後最大的 $ u_j ( j>i,f_j \neq f_i ) $ 。發現 $ f_i \neq f_j $ 很難做到,這裡積累一個 trick ,記錄 $ minn[0][i] $ 為 $ [1,i) $ 中最小值的編號 ,$ minn[1][i] $ 為與 $ minn[0][i] $ 屬於不同連通塊的最小值的編號,這樣就可以快速求出與 $ i $ 不在同一連通塊的最大值。對於 $ j>i $ 同理。實現有些細節,其它沒了。

相關文章