最小生成樹


最小生成樹

 一、什么是圖的最小生成樹(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個連通塊。12處於一個連通塊中,456也處於一個連通塊中,孤立點3也稱為一個連通塊。

Kruskal算法將一個連通塊當做一個集合Kruskal首先將所有的邊按從小到大順序排序(一般使用快排),並認為每一個點都是孤立的,分屬於n個獨立的集合。然后按順序枚舉每一條邊。如果這條邊連接着兩個不同的集合,那么就把這條邊加入最小生成樹,這兩個不同的集合就合並成了一個集合;如果這條邊連接的兩個點屬於同一集合,就跳過。直到選取了n-1條邊為止。

 

算法描述:

 

  1. 初始化並查集father[x]=x
  2. tot=0
  3. 將所有邊用快排從小到大排序。
  4. 計數器 k=0;
  5. 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;

 

  1. 結束,tot即為最小生成樹的總權值之和。

 

 

 

【思想講解】

 

Kruskal(克魯斯卡爾)算法開始時,認為每一個點都是孤立的,分屬於n個獨立的集合。

 

 

5個集合{ {1}{2}{3}{4}{5} }

 

生成樹中沒有邊

 

 

 

Kruskal每次都選擇一條最小的邊,而且這條邊的兩個頂點分屬於兩個不同的集合。將選取的這條邊加入最小生成樹,並且合並集合。

 

第一次選擇的是<1,2>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點12合並成一個集合。

4個集合{ {12}{3}{4}{5} }

生成樹中有一條邊{ <1,2> }

第二次選擇的是<4,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點45合並成一個集合。

3個集合{ {12}{3}{45} }

生成樹中有2條邊{ <1,2> <4,5>}

第三次選擇的是<3,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點35所在的兩個集合合並成一個集合  

2個集合{ {12}{345} }

生成樹中有3條邊{ <1,2> <4,5><35>}

第四次選擇的是<2,5>這條邊,將這條邊加入到生成樹中,並且將它的兩個頂點25所在的兩個集合合並成一個集合。  

 

 

1個集合{ {12345} }

生成樹中有4條邊{ <1,2> <4,5><35><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 }
View Code

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算法采用與DijkstraBellman-Ford算法一樣的“藍白點”思想:白點代表已經進入最小生成樹的點,藍點代表未進入最小生成樹的點。

算法描述:

1為起點生成最小生成樹,min[v]表示藍點v與白點相連的最小邊權。MST表示最小生成樹的權值之和。

一:初始化:min[v]= (v1); 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[2345]=∞。權值之和MST=0

第一次循環自然是找到min[1]=0最小的藍點1。將1變為白點,接着枚舉與1相連的所有藍點234,修改它們與白點相連的最小邊權。

min[2]=w[1][2]=2;

min[3]=w[1][3]=4;

min[4]=w[1][4]=7

第二次循環是找到min[2]最小的藍點2。將2變為白點,接着枚舉與2相連的所有藍點35,修改它們與白點相連的最小邊權。

min[3]=w[2][3]=1;

min[5]=w[2][5]=2;

  

第三次循環是找到min[3]最小的藍點3。將3變為白點,接着枚舉與3相連的所有藍點45,修改它們與白點相連的最小邊權。

 

min[4]=w[3][4]=1;

由於min[5]=2 < w[3][5]=6;所以不修改min[5]的值。

最后兩輪循環將點45以及邊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 }
View Code

 

#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; 是否生成一顆最小生成樹

 

【總結】

克魯斯卡爾

  1. 並查集加排序
  2. 預處理,現將所有的節點的父親指向自己
  3. 輸入m條邊,切記,只需要m條邊
  4. 按每一條邊的權值排序
  5. 最后並查集來查詢,看是否加入到生成樹中,注意並查集模板是這樣寫的

int Find(int x){if(fa[x]==x) return fa[x];  return fa[x]=Find(fa[x]);

  1. 最后查看是否是構造了一顆n個點,n-1條邊的最小生成樹

普里姆

  1. 對於構造邊,就使用鏈式前向星,但是對於邊的話就需要開兩倍
  2. 初始化,將dis[](這個點到最小生成樹的最近的距離)賦值為一個極大的值,但是不能超過INF的二分之一,即為memset(dis,63,sizeof (dis))為一個較大的值,memset賦值的時候需要賦值為2x次方-1
  3. 再用一個pair來儲存隊列節點的信息,宏定義 #define pair<int,int> pp
  4. 放入優先隊列priority_queue<pp,vector<pp>,greater<pp> >  q; 小根堆,默認為大根堆
  5. 兩個連着的尖括號之間需要打空格
  6. Pair的比較,先比較first,在比較second,所以將dis存入firstu存入second
  7. 最外層循環為隊列不為空且最小生成樹還沒有構建好while(!q.empty()&&cnt<n)
  8. 接着,退出隊列中的對頭,查看是否出現過,沒有出現過就將此點打標記,cnt++,答案加上這條邊的權值,擴散連接它的邊,放入隊列

 感謝各位與信奧一本通的鼎力相助!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM