最小生成樹
一、什么是圖的最小生成樹(MST)?
不知道大家還記不記得樹的一個定理:N個點用N-1條邊連接成一個連通塊,形成的圖形只可能是樹,沒有別的可能。
一個有N個點的圖,邊一定是大於等於N-1條的。圖的最小生成樹,就是在這些邊中選擇N-1條出來,連接所有的N個點。這N-1條邊的邊權之和是所有方案中最小的。
二、最小生成樹用來解決什么問題?
就是用來解決如何用最小的“代價”用N-1條邊連接N個點的問題。例如:
【例4-9】、城市公交網建設問題
【問題描述】
有一張城市地圖,圖中的頂點為城市,無向邊代表兩個城市間的連通關系,邊上的權為在這兩個城市之間修建高速公路的造價,研究后發現,這個地圖有一個特點,即任一對城市都是連通的。現在的問題是,要修建若干高速公路把所有城市聯系起來,問如何設計可使得工程的總造價最少?
【輸入格式】
n(城市數,1<=n<=100)
e(邊數)
以下e行,每行3個數i,j,wij,表示在城市i,j之間修建高速公路的造價。
【輸出格式】
n-1行,每行為兩個城市的序號,表明這兩個城市間建一條高速公路。
【輸入樣例】
5 8
1 2 2
2 5 9
5 4 7
4 1 10
1 3 12
4 3 6
5 3 3
2 3 8
【輸出樣例】
1 2
2 3
3 4
3 5
【分析】:
對於最小生成樹問題,有兩種解決方法,*Prim*與*Kruskacl*,復雜度分別為O(m*logn) O(m*logm),一下是對其兩種算法的簡單介紹
Kruskal算法
Kruskal(克魯斯卡爾)算法是一種巧妙利用並查集來求最小生成樹的算法。
首先我們把無向圖中相互連通的一些點稱為處於同一個連通塊中。例如下圖
圖中有3個連通塊。1、2處於一個連通塊中,4、5、6也處於一個連通塊中,孤立點3也稱為一個連通塊。
Kruskal算法將一個連通塊當做一個集合。Kruskal首先將所有的邊按從小到大順序排序(一般使用快排),並認為每一個點都是孤立的,分屬於n個獨立的集合。然后按順序枚舉每一條邊。如果這條邊連接着兩個不同的集合,那么就把這條邊加入最小生成樹,這兩個不同的集合就合並成了一個集合;如果這條邊連接的兩個點屬於同一集合,就跳過。直到選取了n-1條邊為止。
算法描述:
- 初始化並查集。father[x]=x,
- tot=0
- 將所有邊用快排從小到大排序。
- 計數器 k=0;
- for (i=1; i<=M; i++) //循環所有已從小到大排序的邊
if 這是一條u,v不屬於同一集合的邊(u,v)(因為已經排序,所以必為最小),
①合並u,v所在的集合,相當於把邊(u,v)加入最小生成樹。
②tot=tot+W(u,v)
③k++
④如果k=n-1,說明最小生成樹已經生成,則break;
- 結束,tot即為最小生成樹的總權值之和。
【思想講解】
Kruskal(克魯斯卡爾)算法開始時,認為每一個點都是孤立的,分屬於n個獨立的集合。
5個集合{ {1},{2},{3},{4},{5} }
生成樹中沒有邊
Kruskal每次都選擇一條最小的邊,而且這條邊的兩個頂點分屬於兩個不同的集合。將選取的這條邊加入最小生成樹,並且合並集合。
第一次選擇的是<1,2>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點1、2合並成一個集合。
4個集合{ {1,2},{3},{4},{5} }
生成樹中有一條邊{ <1,2> }
第二次選擇的是<4,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點4、5合並成一個集合。
3個集合{ {1,2},{3},{4,5} }
生成樹中有2條邊{ <1,2> ,<4,5>}
第三次選擇的是<3,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點3、5所在的兩個集合合並成一個集合
2個集合{ {1,2},{3,4,5} }
生成樹中有3條邊{ <1,2> ,<4,5>,<3,5>}
第四次選擇的是<2,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點2、5所在的兩個集合合並成一個集合。
1個集合{ {1,2,3,4,5} }
生成樹中有4條邊{ <1,2> ,<4,5>,<3,5>,<2,5>}
算法結束,最小生成樹權值為19。
通過上面的模擬能夠看到,Kruskal算法每次都選擇一條最小的,且能合並兩個不同集合的邊,一張n個點的圖總共選取n-1次邊。因為每次我們選的都是最小的邊,所以最后的生成樹一定是最小生成樹。每次我們選的邊都能夠合並兩個集合,最后n個點一定會合並成一個集合。通過這樣的貪心策略,Kruskal算法就能得到一棵有n-1條邊,連接着n個點的最小生成樹。
Kruskal算法的時間復雜度為O(E*logE),E為邊數。
程序如下:

1 #include<cstdio> 2 #include<iostream> 3 #include<algorithm> 4 #define N 5005 5 #define M 200010 6 #define FORa(i,s,e) for(i=s;i<=e;i++) 7 #define FORs(i,s,e) for(i=s;i>=e;i--) 8 #define File(name) freopen(name".in","r",stdin); freopen(name".out","w",stdout); 9 using namespace std; 10 static char buf[100000],*pa=buf,*pb=buf; 11 #define gc pa==pb&&(pb=(pa=buf)+fread(buf,1,100000,stdin),pa==pb)?EOF:*pa++ 12 inline int read(); 13 struct Edge{ 14 int next,from,to,dis; 15 }edge[N]; 16 int head[10000],n,m,num_edge,fa[N]; 17 bool bz[N]; 18 int find(int x) 19 { 20 if(fa[x]==x) 21 return fa[x]; 22 fa[x]=find(fa[x]); 23 } 24 void Add_edge(int from,int to,int dis) 25 { 26 edge[++num_edge]=(Edge){head[from],from,to,dis}; 27 head[from]=num_edge; 28 } 29 bool cmp(Edge p1,Edge p2) 30 { 31 p1.dis<p2.dis; 32 } 33 int main() 34 { 35 File("Kruskal"); 36 int from,to,fdis,i,tot=0,sum=0,t1,t2; 37 n=read(),m=read(); 38 FORa(i,1,n) fa[i]=i; 39 FORa(i,1,m) 40 { 41 from=read(),to=read(),fdis=read(); 42 Add_edge(from,to,fdis); 43 Add_edge(to,from,fdis); 44 } 45 sort(edge+1,edge+1+n,cmp); 46 bz[1]=1; 47 FORa(i,1,m) 48 { 49 t1=find(edge[i].from),t2=find(edge[i].to); 50 if(t1!=t2) 51 { 52 fa[t1]=fa[t2]; 53 tot++; 54 sum+=edge[i].dis; 55 } 56 if(tot==n-1) break; 57 } 58 if(tot==n-1) 59 cout<<sum; 60 else 61 cout<<"orz"; 62 return 0; 63 } 64 inline int read() 65 { 66 register int x(0);register int f(1);register char c(gc); 67 while(c<'0'||c>'9')f=c=='-'?-1:1,c=gc; 68 while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=gc; 69 return f*x; 70 }
int Find(int x) 並差集 壓縮路徑
{if(fa[x]==x) return x;fa[x]=Find(fa[x]);}
FORa(i,1,n) fa[i]=i; 初始化,將父親指向自己
sort(edge+1,edge+1+n,cmp);排序
FORa(i,1,m)
{
t1=Find(edge[i].from);t2=Find(edge[i].to);
if(t1!=t2) 並差集合並
cnt++,fa[t1]=t2,ans+=edge[i].dis;
if(cnt==n-1)
cout<<ans;return;
Prim算法
Prim算法采用與Dijkstra、Bellman-Ford算法一樣的“藍白點”思想:白點代表已經進入最小生成樹的點,藍點代表未進入最小生成樹的點。
算法描述:
以1為起點生成最小生成樹,min[v]表示藍點v與白點相連的最小邊權。MST表示最小生成樹的權值之和。
一:初始化:min[v]= ∞(v≠1); min[1]=0;MST=0;
二:for (i = 1; i<= n; i++)
1.尋找min[u]最小的藍點u。
2.將u標記為白點
3.MST+=min[u]
4.for 與白點u相連的所有藍點v
if (w[u][v]<min[v])
min[v]=w[u][v];
三:算法結束: MST即為最小生成樹的權值之和
算法分析&思想講解:
Prim算法每次循環都將一個藍點u變為白點,並且此藍點u與白點相連的最小邊權min[u]還是當前所有藍點中最小的。這樣相當於向生成樹中添加了n-1次最小的邊,最后得到的一定是最小生成樹。
我們通過對下圖最小生成樹的求解模擬來理解上面的思想。藍點和虛線代表未進入最小生成樹的點、邊;白點和實線代表已進入最小生成樹的點、邊。
初始時所有點都是藍點,min[1]=0,min[2、3、4、5]=∞。權值之和MST=0。
第一次循環自然是找到min[1]=0最小的藍點1。將1變為白點,接着枚舉與1相連的所有藍點2、3、4,修改它們與白點相連的最小邊權。
min[2]=w[1][2]=2;
min[3]=w[1][3]=4;
min[4]=w[1][4]=7;
第二次循環是找到min[2]最小的藍點2。將2變為白點,接着枚舉與2相連的所有藍點3、5,修改它們與白點相連的最小邊權。
min[3]=w[2][3]=1;
min[5]=w[2][5]=2;
第三次循環是找到min[3]最小的藍點3。將3變為白點,接着枚舉與3相連的所有藍點4、5,修改它們與白點相連的最小邊權。
min[4]=w[3][4]=1;
由於min[5]=2 < w[3][5]=6;所以不修改min[5]的值。
最后兩輪循環將點4、5以及邊w[2][5],w[3][4]添加進最小生成樹。
最后權值之和MST=6。這n次循環,每次循環我們都能讓一個新的點加入生成樹,n次循環就能把所有點囊括到其中;每次循環我們都能讓一條新的邊加入生成樹,n-1次循環就能生成一棵含有n個點的樹;每次循環我們都取一條最小的邊加入生成樹,n-1次循環結束后,我們得到的就是一棵最小的生成樹。這就是Prim采取貪心法生成一棵最小生成樹的原理。算法時間復雜度:O (N2)。

1 #include<cstdio> 2 #include<queue> 3 #include<cstring> 4 #include<utility> 5 #include<algorithm> 6 #define FORa(i,s,e) for(i=s;i<=e;i++) 7 #define R register int 8 using namespace std; 9 10 int n,m,cnt,ans,head[5005],dis[5005],bz[5005]; 11 struct Edge 12 { 13 int next,to,dis; 14 }edge[400005]; 15 int num_edge; 16 void Add_edge(int from,int to,int dis) 17 { 18 edge[++num_edge]=(Edge){head[from],to,dis}; 19 head[from]=num_edge; 20 } 21 typedef pair <int,int> pp; 22 priority_queue <pp,vector<pp>,greater<pp>> q;//first dis second u 23 void Prim() 24 { 25 pp ft; 26 dis[1]=0; 27 q.push(make_pair(0,1)); 28 while(!q.empty()&&cnt<n) 29 { 30 ft=q.top(),q.pop(); 31 if(bz[ft.second]) continue; 32 cnt++,ans+=ft.first,bz[ft.second]=1; 33 for(R i=head[ft.second];i;i=edge[i].next) 34 if(dis[edge[i].to]>edge[i].dis) 35 dis[edge[i].to]=edge[i].dis,q.push(make_pair(dis[edge[i].to],edge[i].to)); 36 } 37 } 38 int main() 39 { 40 memset(dis,127,sizeof(dis)); 41 R from,to,fdis; 42 scanf("%d%d",&n,&m); 43 for(R i=1;i<=m;i++) 44 { 45 scanf("%d%d%d",&from,&to,&fdis); 46 Add_edge(to,from,fdis),Add_edge(from,to,fdis); 47 } 48 Prim(); 49 if (cnt==n)printf("%d",ans); 50 else printf("orz"); 51 }
#define PP pair<int,int>二元組存儲最小生成樹的信息
priority_queue<PP,vector<PP>,greater<PP>> q; 放入優先隊列中,鏈式前向星儲存,降低時空復雜度
memset(dis,63,sizeof(dis)); dis[1]=0; 賦初值
q.push(make_pair(0,1)); 隨便將一個點插入隊列
while(!q.empty()&&cnt<n)
{
p=q.top(),q.pop(); 取出藍點中離最小生成樹最小的點
if(bz[p.second]) continue;判重
cnt++,ans+=p.first,bz[p.second]=1;
for(i=head[p.second];i;i=edge[i].next)
if(edge[i].dis<dis[edge[i].to]) 類似dijkstra的松弛操作,更新藍點離最小生成樹的距離
dis[edge[i].to]=edge[i].dis,q.push(make_pair(edge[i].dis,edge[i].to));
}
if(cnt==n) cout<<ans; 是否生成一顆最小生成樹
【總結】
克魯斯卡爾
- 並查集加排序
- 預處理,現將所有的節點的父親指向自己
- 輸入m條邊,切記,只需要m條邊
- 按每一條邊的權值排序
- 最后並查集來查詢,看是否加入到生成樹中,注意並查集模板是這樣寫的
int Find(int x){if(fa[x]==x) return fa[x]; return fa[x]=Find(fa[x]);
- 最后查看是否是構造了一顆n個點,n-1條邊的最小生成樹
普里姆
- 對於構造邊,就使用鏈式前向星,但是對於邊的話就需要開兩倍
- 初始化,將dis[](這個點到最小生成樹的最近的距離)賦值為一個極大的值,但是不能超過INF的二分之一,即為memset(dis,63,sizeof (dis))為一個較大的值,memset賦值的時候需要賦值為2的x次方-1
- 再用一個pair來儲存隊列節點的信息,宏定義 #define pair<int,int> pp
- 放入優先隊列priority_queue<pp,vector<pp>,greater<pp> > q; 小根堆,默認為大根堆
- 兩個連着的尖括號之間需要打空格
- Pair的比較,先比較first,在比較second,所以將dis存入first,u存入second
- 最外層循環為隊列不為空且最小生成樹還沒有構建好while(!q.empty()&&cnt<n)
- 接着,退出隊列中的對頭,查看是否出現過,沒有出現過就將此點打標記,cnt++,答案加上這條邊的權值,擴散連接它的邊,放入隊列
感謝各位與信奧一本通的鼎力相助!