1. 程式人生 > 資料庫 >MYSQL事件隔離級別以及復讀,幻讀,髒讀的理解

MYSQL事件隔離級別以及復讀,幻讀,髒讀的理解

淺談最小生成樹

            ———\(\rm BiuBiu\_Miku\)

1.一些概念

  · :在一個中,滿足邊數等於點數減一的條件。(如圖1所示)

  · 生成樹:在一個連通圖中,擷取一個子圖,此子圖滿足樹的性質。(如圖2所示)

  · 最小生成樹:在一個包含 \(n\) 個節點的加權連通圖中,所有邊的邊權之和最小的樹,即為最小生成樹。(如圖3所示)



2.例題引入(題皆出自洛谷

【模板】最小生成樹

題目描述

  給出一個無向圖,求出最小生成樹,如果該圖不連通,則輸出 \(orz\)

輸入格式

  第一行包含兩個整數 \(N,M\),表示該圖共有 \(N\)

個結點和 \(M\) 條無向邊。
  接下來 \(M\) 行每行包含三個整數 \(X_i,Y_i,Z_i\) 表示有一條長度為 \(Z_i\) 的無向邊連線結點 \(X_i,Y_i\)

輸出格式

   如果該圖連通,則輸出一個整數表示最小生成樹的各邊的長度之和。如果該圖不連通則輸出 \(orz\)

輸入輸出樣例

輸入

4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3

輸出

7

3.演算法實現

   關於演算法的實現一般有兩種演算法,第一種稱為 \(Kruskal\) ,第二種稱為 \(Prim\)

   實現方法一.\(Kruskal\) 時間複雜度 \(O( MlogM )\)
\(M\)為邊數)

     \(Kruskal\) 是一種利用並查集來實現最小生成樹的辦法,其演算法流程大致為。

     先將讀進來的邊按照邊權排序,然後利用並查集建立關係,一但發現如果兩個點不存在關係,那麼就建這條邊,不然的話就不建,最後把建了邊的邊權統計起來就好了。

     為什麼這樣就可以找到最小生成樹呢?因為我們已經將邊權排序過了,也就是說越前面的邊他的邊權就越小,因此我們就可以直接通過簡單的步驟得到最小生成樹了!

     以上面的題目的樣例來做例子,來模擬一下該演算法全過程:

      1.排序,排序後得到以下的資料

1 2 2
1 3 2
1 4 3
3 4 3
2 3 4

      2.發現 \(1\)\(2\) 不連通,於是建邊,如下圖所示:

      

      3.發現 \(1\)\(3\) 不連通,於是建邊,如下圖所示:

      

      4.發現 \(1\)\(4\) 不連通,於是建邊,如下圖所示:

      

      5.發現 \(3\)\(4\) 已經連通,於是不建邊。

      6.發現 \(2\)\(3\) 已經聯通,於是不建邊。

      7.經統計,一共建的邊的邊權之和為 \(7\) 於是就輸出 \(7\)

     \(Code:\)
#include<bits/stdc++.h>
using namespace std;
int f[200005],m;
struct edge{
	int a,b,lo;
}E[200005];	//邊 
int n,ans;
bool cmp(edge x,edge y){return x.lo<y.lo;} //排序 
int getfather(int k){ //並查集 
	if(f[k]==k)return k;
	return f[k]=getfather(f[k]);
}
int main(){
    scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)f[i]=i;
	for(int i=1;i<=m;i++)scanf("%d%d%d",&E[i].a,&E[i].b,&E[i].lo);
	sort(E+1,E+1+m,cmp); //排序 
	for(int i=1;i<=m;i++){
		if(getfather(E[i].a)!=getfather(E[i].b)){	//判斷兩個點是否連通 
			ans+=E[i].lo;	//若不連通建邊 
			f[getfather(E[i].a)]=getfather(E[i].b);
			n--;	//減掉一個未連通點 
		}
		if(n==1){	//若全部連通就輸出答案 
        	printf("%d\n",ans);
			return 0;
		}
	}
	printf("orz\n");	//否則輸出orz 
	return 0;
} 

   實現方法二.\(Prim\) (時間複雜度根據不同情況,不同計算)

     \(Prim\) 是一種利用不斷尋找最小值來實現找到最小生成樹的方法。

     具體來說就是以某一個點為起點(一般選擇節點 \(1\) 為起點),尋找能走的邊中邊權(代價)最小的邊,再將此點收入囊中,也就是看做是一段子圖。

     然後更新能走的邊所花費的代價,因為當我加入一個節點後,該節點也可以通到別的點,而且從此新節點出發走原來可以走的邊,可能存在更小代價.

    例如:若此時 \(A\) 已經與 \(B\) 已經連線,\(A\)\(C\) 代價為 \(3\)\(B\) 存在一條邊到 \(C\) 的代價為 \(1\) ,那麼此時就可以更新當前可以走的邊。

     以上面的題目的樣例來做例子,來模擬一下\(Prim\) 演算法全過程:

      1.選擇節點 \(1\) 為起點,並發現當前 \(1\)\(2\)\(1\)\(3\) 都是當前能走的邊中邊權最小的,於是任意走一條即可(這裡選擇走 \(1\)\(2\) ),如下圖所示。

      

      2.當前能走的邊中發現 \(1\)\(3\) 是邊權最小的邊,於是走此邊,如下圖所示。

      

      3.當前能走的邊中,發現當前 \(1\)\(4\)\(3\)\(4\) 都是當前能走的邊中邊權最小的,於是任意走一條即可(這裡選擇走 \(1\)\(4\) ),如下圖所示。

      

      4.找到最小生成樹,經統計,走過的邊權總和為 \(7\) 於是輸出 \(7\)

     \(Code:\) (這裡用鄰接矩陣存圖,複雜度為 \(O(N^2)\) N表示點的數量 )
#include<bits/stdc++.h>
using namespace std;
int n,mmin=INT_MAX,dis[200005],ans,w[5005][5005],walk,m;	
bool vis[200005];	//vis表示該邊是否被訪問過 
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			w[i][j]=1e9;		//矩陣初始化 
	for(int i=1;i<=m;i++){
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		w[a][b]=min(c,w[a][b]);
		w[b][a]=min(c,w[b][a]); 	//鄰接矩陣存圖 
	}
	for(int i=1;i<=n;i++)dis[i]=w[1][i];	//初始化從起點開始能到達的邊,能到達就賦值走到該點要付出的代價,否則為1e9 
	dis[1]=0;
	vis[1]=true;
	for(int i=2;i<=n;i++){
		mmin=1e9;				
		for(int j=2;j<=n;j++)
			if(dis[j]<mmin&&!vis[j]){	//找到當前所有能走的邊的最小值 
				mmin=dis[j];
				walk=j;
			}
		ans+=mmin;			//走這條邊 
		vis[walk]=true;		//標記已經走過 
		for(int j=2;j<=n;j++)if(!vis[j])dis[j]=min(dis[j],w[walk][j]); //更新能走的邊花費的價值 
	} 
	printf("%d\n",ans); 
	return 0; 
} 

4.題目推薦

  [USACO05MAR]Out of Hay S:相當於模板(雙倍經驗,豈不美哉)
  [USACO3.1]最短網路 Agri-Net:本題用 \(for\) 建邊後跑流程即可。
  公路修建:本題要運用兩點之間的距離公式 \(\sqrt{( x_1 - x_2 )^2+( y_1 - y_2 )^2}\) 來計算邊權,然後跑流程就好了,本題推薦使用 \(Prim\) 來實現。

  \(PS\) :以上題目適合初學者食用。

5.結語

  以上便是此部落格的全部內容,其主要適用於沒學過最小生成樹的人用,有什麼寫得不好也歡迎大佬在評論區指出,感謝大佬的閱讀。