1. 程式人生 > >最小生成樹(prim和kruskal)

最小生成樹(prim和kruskal)

問題引入:
假設要在n個城市之間建立通訊網路,則連通n個城市至少需要n-1條線路。n個城市之間,最多可能設定n*(n-1)/2條線路。這時,自然會考慮一個問題:如何在這些可能的線路中選擇n-1條,使得在最節省費用的前提下建立該網路通訊?
解決方法:
1.prim演算法
演算法思想:
G=(V,E)是無向連通帶權圖,V=(1,2,3,…,n);設最小生成樹T=(U,TE),演算法結束時U=V,TE屬於E。構造最小生成樹T的prim演算法思想是:首先令U={u0},u0屬於V,TE={}。然後,只要U是V的真子集,就做如下貪心選擇:選取滿足條件i屬於U,j屬於V-U,且邊(i,j)是連線U和V-U的所有邊中的最短邊,即該邊的權值最小。然後,將頂點j加入集合U,邊(i,j)加入結合TE。繼續上面的貪心選擇一直到U=V為止,此時選取到的所有的邊恰好構成G的一棵最小生成樹T。(看成是一個集合到另外一個集合,逐次從中選出一個路線)
演算法設計

  1. 將資料存放在陣列,如果兩個點不通,則設定為無窮大(0x33f3f3f)
  2. 初始化陣列lowcost[],假設從1號點(下標1,lowcost[0]=0)開始,將與1相連的邊存放到陣列lowcost[]中
  3. 尋找到與1相連的最小的邊,記錄其在lowcost陣列中的小標和權值,
#include<cstdio>
#include<cstring>
using namespace std;
int lou[506][506],v,e,total;
const int inf=0x3f3f3f3f;
void prim()
{
   int lowcost[506],flag,minn;//用於記錄每次加入一個點後連通其他點的最小權值,
lowcost[1]=0; for(int i=2;i<=v;i++)//假設加入了點1,然後給lowcost賦值與1連線的邊權值 { lowcost[i]=lou[1][i]; } for(int i=1;i<v;i++)//只需要尋找n-1條邊 { flag=0,minn=inf; for(int j=1;j<=v;j++)//找出當前的點集合中最小的邊 { if(lowcost[j]&&lowcost[j]<minn) { minn=lowcost[j];//記錄最小的權值
flag=j;//標記位置 } } if(flag!=0) total+=minn; lowcost[flag]=0; for(int i=2;i<=v;i++) //更新lowcost[]陣列 { if(lowcost[i]&&lowcost[i]>lou[flag][i]) { lowcost[i]=lou[flag][i]; } } } } int main() { int n,out[506],a,b,c; scanf("%d",&n); while(n--) { memset(out,0,sizeof(out)); memset(lou,inf,sizeof(lou));//開始賦初值為整型無窮大量 scanf("%d%d",&v,&e); total=0; for(int i=0;i<e;i++) { scanf("%d%d%d",&a,&b,&c); lou[a][b]=c; lou[b][a]=c; } prim(); int sm=inf; for(int i=0;i<v;i++) { scanf("%d",&out[i]); if(sm>out[i])sm=out[i]; } printf("%d\n",total+sm); } return 0; }

2.kruskal演算法

演算法思想:
kruskal演算法將n個點看成是n個孤立的連通分支。它首將所有的邊按權從小到大排序。然後,只要T中的連通分支數目不為1,就做如下貪心選擇:在邊集E中選取權值最小的邊(i,j),如果將邊(i,j)加入集合TE中不產生迴路(環),則將邊(i,j)加入邊集TE中,即用邊(i,j)將這兩個連通分支合併連線成一個連通分支,否則繼續選擇下一條最短邊。在這兩種情況下,把邊(i,j)從集合中刪除。繼續上面的貪心選擇知道T中的所有頂點都在同一個連通分支上為止。此時,選取到的n-1條邊恰好構成G的一棵最下生成樹T。
演算法設計:

  1. 初始化,將圖G的邊集E中的所有邊按權從大到小排序,邊集TE={},把每個頂點都初始化為一個孤立的分支,即一個頂點對應的集合。
  2. 在E中尋找權值最小的邊(i,j)。
  3. 如果頂點i和j位於兩個不同的連通分支,則將邊(i,j)加入邊集TE,並執行合併操作將兩個連通分支進行合併。
  4. 將邊(i,j)從集合E中刪去,即E=E-{(i,j)}
  5. 如果連通分支數目不為1,轉步驟2;否則演算法結束,生成最小生成樹T。
    程式碼:
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
    int a,b,cost;
}build[130000];
bool cmp(building x,building y)//讓點之間的距離按照花費從小到大排序
{
    return x.cost<y.cost;
}
int main()
{
    int t,v,e;
    scanf("%d",&t);
    while(t--)
    {
        int wei[506],weight=0;
        memset(build,0,sizeof(build));
        memset(fa,0,sizeof(fa));
        scanf("%d%d",&v,&e);
        for(int i=0;i<e;i++)
        {
            scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
        }
        sort(build,build+e,cmp);//用sort排序
//        for(int j=0;j<e;j++)
//        {
//            printf("%d %d\n",build[j].a,build[j].cost);
//        }
        for(int i=0;i<v;i++)
        {
            scanf("%d",&wei[i]);
            fa[i]=i;
        }
        sort(wei,wei+v);
        for(int k=0;k<e;k++)
        {
          int x=fa[build[k].a-1];//此處也不是很明白
          int y=fa[build[k].b-1];
          if(x!=y)//當前最小邊,如果不為環,那麼加入該集合
          {
              weight+=build[k].cost;
              //printf("====weight=%d\n",weight);
              for(int i=0;i<v;i++)
              {
                  if(fa[i]==y)//把可以相連的點併入一個集合,通過設定它們的fa[]值相同實現
                  {
                      fa[i]=x;
                  }
              }
          }
        }
        printf("%d\n",wei[0]+weight);


    }
    return 0;
}

如果G中的邊比較少,可以採用kruskal,如果邊數較多,則適用prim演算法。時間上,kruskal時間複雜度為O(eloge),prim演算法時間複雜度為O(n^2),prim適用於稠密圖,kruskal適用於稀疏圖。
另外kruskal演算法另外一種通過並查集的方式寫
並查集:
我們可以把每個連通分量看成一個集合,該集合包含了連通分量的所有點。而具體的連通方式無關緊要,好比集合中的元素沒有先後順序之分,只有“屬於”與“不屬於”的區別。圖的所有連通分量可以用若干個不相交集合來表示。

而並查集的精妙之處在於用數來表示集合。如果把x的父結點儲存在p[x]中(如果沒有父親,p[x]=x),則不難寫出結點x所在樹的遞迴程式:

find(int x) {return p[x]==x?x:p[x]=find(p[x]);}

意思是,如果p[x]=x,說明x本身就是樹根,因此返回x;否則返回x的父親p[x]所在樹的根結點。

既然每棵樹表示的只是一個集合,因此樹的形態是無關緊要的,並不需要在“查詢”操作之後保持樹的形態不變,只要順便把遍歷過的結點都改成樹根的兒子,下次查詢就會快很多了
程式碼:


#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
    int a,b,cost;
}build[130000];
bool cmp(building x,building y)
{
    return x.cost<y.cost;
}
int find(int x)
{
    //printf("-->%d\n",x);
    if(x!=fa[x-1])fa[x-1]=find(fa[x-1]);//並查集
    return fa[x-1];
}
int main()
{
    int t,v,e;
    scanf("%d",&t);
    while(t--)
    {
        int wei[506],weight=0;
        memset(build,0,sizeof(build));
        memset(fa,0,sizeof(fa));
        scanf("%d%d",&v,&e);
        for(int i=0;i<e;i++)
        {
            scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
        }
        sort(build,build+e,cmp);
//        for(int j=0;j<e;j++)
//        {
//            printf("%d %d\n",build[j].a,build[j].cost);
//        }
        for(int i=0;i<v;i++)
        {
            scanf("%d",&wei[i]);
            fa[i]=i+1;
        }
        sort(wei,wei+v);
        for(int k=0;k<e;k++)
        {
          int x=find(build[k].a);
          int y=find(build[k].b);
          //printf("%d %d\n",x,y);
          if(x!=y)
          {
              weight+=build[k].cost;
              fa[x-1]=y;
             // printf("%d\n",weight);
          }
        }
        printf("%d\n",wei[0]+weight);
    }
    return 0;
}