圖論算法(七)最小生成樹Kruskal算法與(嚴格)次小生成樹

吐槽:嚴格次小生成樹我調了將近10個小時……(雖然有3個小時的電競時間html

Part 1:最小生成樹\(Kruskal\)算法

前言

若是您還不知道最小生成樹的定義是什麼,請空降這裏ios

介於咱們以前已經討論過了最小生成樹的定義和\(Prim\)算法了,此次咱們直奔主題——\(Kruskal\)算法算法

\(Kruskal\)算法工做方式

仍是老樣子,咱們拋開正確性不談,只談算法工做方式和代碼實現數組

\(Kruskal\)的思路很是暴躁:把每一個點當作是一個單獨的連通塊,而後把邊按照邊權排序,從小到大一條一條加入最小生成樹,這樣直到有\(n-1\)條邊被加入了最小生成樹數據結構

注意每次加入邊的時候,至少有原來不連通的兩連通塊被聯通,這樣纔可以保證不會造成環框架

爲何不知足上面的條件就會造成環?ui

顯然易見,若是兩個點在加入這條邊以前,已經能夠相互到達了(成爲一個連通塊),那麼這條邊就是多餘的,在鏈接以後會造成重邊或者環
每個連通塊都是一個無根樹,在樹上任意兩點之間加一條邊,必定會造成一個環,因此爲了避免造成環,咱們不加這條邊spa

讓咱們舉一個生動形象的例子(由於電腦畫圖實在不方便,這裏改爲手畫了)

好比像上面這張帶權無向圖:debug

咱們模擬\(Kruskal\),每一個點全都分開,從小到大加入邊

重複這個操做一直從小到大的加邊,直到完成最小生成樹
3d

而後這樣咱們就獲得了這個圖的最小生成樹(不惟一),它的大小爲\(7\)

\(Kruskal\)算法實現方式

在算法中,有一個很重要的步驟,就是維護每一條邊連通的兩個點是否是處於同一個連通塊以內,若是處於同一連通塊以內,那麼這條邊是不能被加入最小生成樹的

因此咱們須要一種數據結構來維護各個連通塊的狀況,而並查集就很好的知足了咱們的要求

PS:不知道什麼是並查集?請戳這裏(請從其餘園友那裏搜叭)

維護一個有\(n\)個集合(每一個集合表明一個點)的並查集,初始時每一個集合中只有一個元素(點\(i\)

\(一、\)對於每次加邊以前,查詢兩個端點\(a,b\)是否在同一集合裏,若是不在同一集合裏,執行\(2\),若是在同一集合,那麼說明\(a,b\)已經連通了,跳過這條邊

\(二、\)把這條邊計入最小生成樹,而後合併點\(a,b\)所在的集合

\(Code\)

#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
#include<set>
#include<map>
#include<utility>
#include<iostream>
#include<list>
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<iomanip>
typedef long long int ll;
inline int read(){
	int fh=1,x=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
	return fh*x;
}
inline int _abs(const int x){ return x>=0?x:-x; }
inline int _max(const int x,const int y){ return x>=y?x:y; }
inline int _min(const int x,const int y){ return x<=y?x:y; }
inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }

const int maxn=5005;
const int inf=1e9;

int n,m;

struct Edge{
	int a,b,w;
	Edge(){}
}edge[200005];//創建一個結構體,存邊的兩個端點和權值 
bool operator < (const Edge p,const Edge q){ return p.w<q.w; }
//重載運算符,按照權值從小到大排序 

int fa[maxn];
int find(const int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}//並查集的查詢操做,查詢x的集合表明 
inline int Kruskal(){
	int ans=0;//最小生成樹大小 
	std::sort(edge,edge+m);//排個序 
	for(int i=1;i<=n;i++)//並查集初始化,每一個點都是獨立的連通塊 
		fa[i]=i;
	for(int i=0;i<m;i++){//把全部邊掃一遍 
		int x=edge[i].a,y=edge[i].b;
		if(find(x)==find(y)) continue;//若是屬於一個集合,就跳出 
		fa[find(x)]=y;//合併兩個集合 
		ans+=edge[i].w;//統計最小生成樹大小 
	}
	return ans;
}

int main(){
	n=read(),m=read();
	for(int i=0,x,y,z;i<m;i++){
		x=read(),y=read(),z=read();
		if(x==y) continue; 
		edge[i].a=x;
		edge[i].b=y;
		edge[i].w=z;//建圖 
	}
	printf("%d\n",Kruskal());//跑最小生成樹 
	return 0;
}

Part 2:求解(嚴格)次小生成樹

傳送門:【模板】嚴格次小生成樹

什麼是次小生成樹

字面意思來說,次小生成樹就是除了最小生成樹以外最小的那棵生成樹

那麼爲何把「嚴格」加了個括號呢?由於它並不嚴格(廢話)

若是最小生成樹選擇的邊集是\(E_M\),嚴格次小生成樹選擇的邊集是\(E_S\),那麼須要知足:\((value(e)\)表示邊\(e\)的權值\()\)

\(\sum_{e \in E_M}value(e)<\sum_{e \in E_S}value(e)\)

顯然這個公式它比較噁心,簡單來講,就是次小生成樹的大小必須嚴格小於最小生成樹的大小

若是咱們只是在最小生成樹上更換了邊,可是總的生成樹大小同樣,那麼求出的就是不嚴格的次小生成樹(由於一張圖的最小生成樹能夠有多個,此時求出了另外一個最小生成樹)

求嚴格次小生成樹(思路篇)

看到「嚴格次小生成樹」,你的第一反應應該是「這個東西會和最小生成樹有關!」

所謂「次小」就是除了最小以外的最小的,那麼咱們先求出最小的,起碼不會對咱們的求解起阻礙做用吧

假設你已經順手使用\(Prim\)或者\(Kruskal\)算法求出了這張圖的最小生成樹

如今,咱們能夠把這個圖上的全部邊簡單的分紅兩類了——第一類是在最小生成樹上的邊(簡稱樹邊),第二類是不在最小生成樹上的邊(簡稱非樹邊)

仍是一開始的那個圖,最小生成樹上的邊使用藍筆標出,咱們已知這個最小生成樹的大小\(size=7\)

能夠貪心的轉化問題——求第二小,也就是求一個新的生成樹大小\(size'\)在比\(size\)小的前提下最大

由於嚴格次小生成樹和最小生成樹必定不是同一棵樹,那麼咱們能夠考慮用一些非樹邊,替換掉同等數量的樹邊

想到替換時,也許你會遇到這些問題,(如下用三元組\((a,b,c)\)表示連通\(a、b\)兩點的邊,權值爲\(c\)

1. 替換時,怎麼保證求出的是\(2\)小而不是\(3\)小、\(4\)小、\(n\)小呢?

根據上面的貪心思路:替換後使得\(size'-size\)最小,那麼\(size'\)就是嚴格次小生成樹

2. 一條非樹邊能夠替換掉哪些樹邊?

顯然不能夠胡亂替換,由於須要保證求出的次小生成樹還有個樹的模樣(不能出現環、必須連通)

想到利用樹的性質:當咱們要向一棵樹中加入一條邊時,會造成且僅造成一個環,而且這個環包含\(a\rightarrow LCA(a,b)\)\(b\rightarrow LCA(a,b)\)路徑中的全部邊

咱們先把一條非樹邊\((a,b,c)\)加進去,在\(a\rightarrow LCA(a,b)\)\(b\rightarrow LCA(a,b)\)路徑中的邊中刪除一條便可保證樹的性質

上面的解釋理解不能?請看下面的圖,幫助理解:

如圖,黑色的是樹邊,如今要把一條綠色的非樹邊\((8,6,n)\)加進去,那麼圖中標紅的樹邊就能夠被替換

書面證實

對於一棵樹\(T\)其中任意兩個節點\(a,b\)都存在且僅存在一條簡單路徑:\(a\rightarrow LCA(a,b)\rightarrow b\)
如今鏈接\((a,b)\),那麼上式能夠寫成這樣:\(a\rightarrow LCA(a,b)\rightarrow b\rightarrow a\),顯然構成一個環
PS:另外,由於鏈接\((a,b)\)以前路徑只有一條,因此鏈接後,造成的環也只有一個

證畢

3. 用幾條非樹邊替換幾條樹邊呢?

在解決這個問題以前,先給出另外一個結論:

對於任意一條非樹邊\((a,b,c)\),在把它加入最小生成樹後造成的環中,每一條樹邊的權值\(w\leq c\)

解決這個問題,須要用到這個結論,因此咱們先來證實一下它

證實

設加入的非樹邊是\((a,b,c)\),產生環包含樹邊的集合是\(T\)\(w_e\)表示\(e\)的邊權,最小生成樹的大小爲\(size\)
反證法:
\(p:\exists e\in T\)且有\(w_e>c\)
假設\(p\)爲真,那麼此時斷掉樹邊\(e\),加入非樹邊\((a,b,c)\)會使得\(size\)減少
可是\(size\)是咱們所求出的最小生成樹的大小,它不可能更小了,因此\(p\)爲假
那麼\(!p:\forall e\in T w_e\leq c\)必定爲真

證畢

有了上面的結論還不夠,要想繼續解決這個問題,咱們還得大膽猜測,當心求證

作出假設:替換\(1\)條邊最優

證實

還記得咱們的貪心思路嗎:替換後使得\(size'-size\)最小,那麼\(size'\)就是嚴格次小生成樹
咱們假設用一條非樹邊\(e_1\)去替換一條樹邊,運用上面的結論,咱們知道全部與\(e_1\)構成環的邊的權值\(w\geq w_{e_1}\)
若是咱們作此次替換,\(size'\)的大小就會比\(size\)\(k=w-w_{e_1}\)
考慮特殊狀況,\(k=0\)時,表明着咱們把兩條權值相等的樹邊和非樹邊作了替換
可是這麼作對\(size'\)沒有任何影響,只對次小生成樹的形態有影響,但它長得好很差看咱們並不關心,咱們只關心\(size'\)而已
由於\(k=0\)時,對\(size'\)沒有影響,排除這種狀況,如今\(k\)的範圍\((k>0)\)
(注意咱們之後都再也不考慮\(k=0\)的狀況了!!!)
顯然咱們選擇\(n\)條邊,就會產生多個\(k\)值,此時\(size'\)必定大於只選\(1\)條邊的\(size'\)
根據貪心思路,要求\(size'\)最小,顯然選\(1\)條邊作替換最優

證畢

問題解決,選擇\(1\)條邊作替換最優(也就是最小生成樹和次小生成樹中權值不相同的邊有且只有\(1\)條)

4. 用哪條非樹邊替換哪條樹邊?答案怎麼更新?
這兩個咱們逃不掉得枚舉其中一個了,這裏顯然枚舉非樹邊比較方便(設樹邊邊權爲\(w\))

由於咱們要求\(size'\)最大的,因此要求\((c-w>0)\)\((c-w)\)儘可能小,由於\(c\)必定,只要使得\(w\)儘可能大便可(注意這裏根據上面的結論,\(c\geq w\)因此不用擔憂\(c-w<0\)的狀況)

枚舉每個非樹邊\((a,b,c)\),找到路徑\(a\rightarrow LCA(a,b)\)\(b\rightarrow LCA(a,b)\)\(w\)最大的那條邊,作替換便可,答案就是\(size'=min(size',size+c-w)\)

注意一下小細節:由於\(w\)有可能等於\(c\),若是作替換的話,求出的並非嚴格次小生成樹,因此還須要記錄一個次大值\(w'\),若是\(w=c\),那麼答案\(size'=min(size',size+c-w')\)

囉囉嗦嗦這麼多,按照上面的思路,就能夠求嚴格次小生成樹了

求嚴格次小生成樹(實現篇)

明確了思路,有沒有感到幹勁十足呢?(必須得幹勁十足啊,筆者但是\(1\)小時寫完,\(7\)小時\(debug\)的男人)

這裏梳理一下咱們要用的算法和數據結構吧:

1. 求最小生成樹:並查集,\(Kruskal\),無腦存邊

2. 最近公共祖先\(LCA\):樹上倍增,\(dfs\)預處理,鄰接表

3. 區間最大/次大\(RMQ\):樹上\(st\)

4. 一句提醒:不開\(long\) \(long\)見祖宗

因此獲得公式:基礎算法+數據結構+簡單證實=毒瘤題(這題其實出的挺好的,既沒超綱,又有必定難度)

大致框架有了,剩下的都是代碼實現的細節了,就都在代碼註釋裏講解吧

求嚴格次小生成樹(代碼篇)

#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
#include<set>
#include<map>
#include<utility>
#include<iostream>
#include<list>
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<iomanip>
typedef long long int ll;
inline int read(){
	int fh=1,x=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
	return fh*x;
}
inline int _abs(const int x){ return x>=0?x:-x; }
inline ll _max(const ll x,const ll y){ return x>=y?x:y; }
inline ll _min(const ll x,const ll y){ return x<=y?x:y; }
inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }

const int maxn=100010;
const ll inf=1e18;//賦個極大值 

int n,m;
ll MST,ans=inf;//ans是次小生成樹,MST是最小生成樹 

struct Node{
	int to;//到達點 
	ll cost;//邊權 
};
std::vector<Node>v[maxn];//鄰接表存最小生成樹 
int dep[maxn];//dep[i]表示第i個點的深度 
bool vis[maxn];
ll f[maxn][25];//f[i][j]存節點i的2^j級祖先是誰 
ll g1[maxn][25];//g1[i][j]存節點i到他的2^j級祖先路徑上最大值 
ll g2[maxn][25];//g2[i][j]存節點i到他的2^j級祖先路徑上次大值  
void dfs(const int x){//由於是樹上st和樹上倍增,因此能夠一塊兒預處理 
	vis[x]=true;//x號點訪問過了 
	for(int i=0;i<v[x].size();i++){//掃全部出邊 
		int y=v[x][i].to; 
		if(vis[y]) continue;//兒子不能被訪問過 
		dep[y]=dep[x]+1;//兒子的深度是父親+1 
		f[y][0]=x;//兒子y的2^0級祖先是父親x 
		g1[y][0]=v[x][i].cost;//y到他的2^0級祖先的最大邊長 
		g2[y][0]=-inf;//y到他的2^0級祖先的次大邊長(沒有次大邊長,故爲-inf) 
		dfs(y);//遞歸預處理 
	}
}
inline void prework(){//暴力預處理 
	for(int i=1;i<=20;i++)//枚舉2^1-2^20 
		for(int j=1;j<=n;j++){//枚舉每一個點 
			f[j][i]=f[f[j][i-1]][i-1];//正常的倍增更新 
			g1[j][i]=_max(g1[j][i-1],g1[f[j][i-1]][i-1]);
			g2[j][i]=_max(g2[j][i-1],g2[f[j][i-1]][i-1]);
			//如下是求次大的精華了 
			if(g1[j][i-1]>g1[f[j][i-1]][i-1]) g2[j][i]=_max(g2[j][i],g1[f[j][i-1]][i-1]);
			//j的2^i次大值,是j的2^(i-1)和j^2(i-1)的2^(i-1)最大值中的較小的那一個 
			//特別的,若是這兩個相等,那麼沒有次大值,不更新g2數組 
            else if(g1[j][i-1]<g1[f[j][i-1]][i-1]) g2[j][i]=_max(g2[j][i],g1[j][i-1]);
		}
}
inline void LCA(int x,int y,const ll w){
//非樹邊鏈接x,y權值爲w 
//求LCA時候直接更新答案 
	ll zui=-inf,ci=-inf;//zui表示最大值,ci表示次大值 
	if(dep[x]>dep[y]) std::swap(x,y);//保證y比x深 
	for(int i=20;i>=0;i--)//倍增向上處理y 
		if(dep[f[y][i]]>=dep[x]){
			zui=_max(zui,g1[y][i]);//更新路徑最大值 
			ci=_max(ci,g2[y][i]);//更新路徑次大值 
			y=f[y][i];
		}
	if(x==y){
		if(zui!=w) ans=_min(ans,MST-zui+w);//若是最大值和w不等,用最大值更新 
		else if(ci!=w&&ci>0) ans=_min(ans,MST-ci+w);//有毒瘤狀況,沒有次大值,此時也不能用次大值更新 
		return; 
	}
	for(int i=20;i>=0;i--)
		if(f[x][i]!=f[y][i]){
			zui=_max(zui,_max(g1[x][i],g1[y][i]));
			ci=_max(ci,_max(g2[x][i],g2[y][i]));
			x=f[x][i];
			y=f[y][i];
		}//依舊是普通的更新最大、次大值 
	zui=_max(zui,_max(g1[x][0],g1[y][0]));//更新最後一步的最大值 
	//注意下面這兩句又凝結了人類智慧精華
	//由於次大值有可能出如今最後一步上,因此在更新答案前還要更新一下ci
	//若是最後兩邊的某一邊是最大值,ci就只能對另外一邊取max  
	if(g1[x][0]!=zui) ci=_max(ci,g1[x][0]);
	if(g2[y][0]!=zui) ci=_max(ci,g2[y][0]);
	if(zui!=w) ans=_min(ans,MST-zui+w);
	else if(ci!=w&&ci>0) ans=_min(ans,MST-ci+w);//依舊特判毒瘤狀況 
}

struct Edge{
	int from,to;//鏈接from和to兩個點 
	ll cost;//邊權 
	bool is_tree;//記錄是否是樹邊 
}edge[maxn*3];
bool operator < (const Edge x,const Edge y){ return x.cost<y.cost; }
//重載運算符,按照邊權從大到小排序 
int fa[maxn];//並查集數組 
inline int find(const int x){
	if(fa[x]==x) return x;
	else return fa[x]=find(fa[x]);
}//查詢包含x的集合的表明元素 
inline void Kruskal(){
	std::sort(edge,edge+m);//先排序 
	for(int i=1;i<=n;i++)//初始化並查集 
		fa[i]=i;
	for(int i=0;i<m;i++){
		int x=edge[i].from;
		int y=edge[i].to;
		ll z=edge[i].cost;
		int a=find(x),b=find(y);
		if(a==b) continue;//若是x和y已經連通,continue掉 
		fa[find(x)]=y;//合併x,y所在集合 
		MST+=z;//求最小生成樹 
		edge[i].is_tree=true;//標記爲樹邊 
		v[x].push_back((Node){y,z});//鄰接表記錄下樹邊 
		v[y].push_back((Node){x,z});
	}
}

int main(){
	n=read(),m=read();
	for(int i=0,x,y;i<m;i++){
		ll z;
		x=read(),y=read();scanf("%lld",&z);
		if(x==y) continue;
		edge[i].from=x;
		edge[i].to=y;
		edge[i].cost=z;
	}//讀入整個圖 
	Kruskal();//初始化最小生成樹 
	dep[1]=1;//設1號點是根節點,把它變成有根樹 
	dfs(1);//從1開始預處理 
	prework();//倍增預處理 
	for(int i=0;i<m;i++)//枚舉全部邊 
		if(!edge[i].is_tree)//若是是非樹邊,那麼更新答案 
			LCA(edge[i].from,edge[i].to,edge[i].cost);
	printf("%lld\n",ans);//輸出答案,不開long long見祖宗 
	return 0;//我諤諤終於結束 
}

不嚴格次小生成樹

嚴格的都說了,那就再稍微說一兩句不嚴格次小生成樹的求法

剛纔那麼多的證實,就是爲了讓這個不嚴格變成嚴格,如今咱們倒過來看,這個問題就變得小兒科了

總體框架不用變,只是咱們不須要處理次大值了,每次更新的時候直接\(ans=min(ans,ans+c-w);\)便可

感謝您的閱讀,來個三連球球辣\(OvO\)

相關文章
相關標籤/搜索