1. 程式人生 > >ac之最小生成樹的兩種經典演算法

ac之最小生成樹的兩種經典演算法

傳送門

佈線問題

時間限制:1000 ms  |  記憶體限制:65535 KB難度:4
描述
南陽理工學院要進行用電線路改造,現在校長要求設計師設計出一種佈線方式,該佈線方式需要滿足以下條件:
1、把所有的樓都供上電。
2、所用電線花費最少
輸入
第一行是一個整數n表示有n組測試資料。(n<5)
每組測試資料的第一行是兩個整數v,e.
v表示學校裡樓的總個數(v<=500)
隨後的e行裡,每行有三個整數a,b,c表示a與b之間如果建鋪設線路花費為c(c<=100)。(哪兩棟樓間如果沒有指明花費,則表示這兩棟樓直接連通需要費用太大或者不可能連通)
隨後的1行裡,有v個整數,其中第i個數表示從第i號樓接線到外界供電設施所需要的費用。( 0<e<v*(v-1)/2 )
(樓的編號從1開始),由於安全問題,只能選擇一個樓連線到外界供電裝置。
資料保證至少存在一種方案滿足要求。
輸出
每組測試資料輸出一個正整數,表示鋪設滿足校長要求的線路的最小花費。
樣例輸入
1
4 6
1 2 10
2 3 10
3 1 10
1 4 1
2 4 1
3 4 1
1 3 5 6
樣例輸出
4

//因為定點數比較少在,這個因為普里母演算法的時間複雜度是O(n ^2),所以說整個演算法大約是250000左右可以被執行的,因為這種演算法的時間複雜度比較依賴於頂點數目也就是說,如果頂點較多那麼請放棄普里母演算法,可以進行克魯斯卡爾演算法

#include<iostream>//引入標準的輸入輸出函式
#include<string.h>//需要用到memset,memset()可以用在字元陣列的初始化以及類似於memset(arr,0,n*sizeof(int));的情況,效率比手動迴圈賦值要高的多。
#define MAXNUM 0x3f3f3f3f//初始化最大值,這是因為memset是按位元組進行填充的,每個位元組填充0x3f恰好不會使一個int溢位
#define MAXN 505//定義最大的頂點數量
using namespace std;//引入標準名稱空間
int n,e;//定義全域性的頂點個數和邊數
int graph[MAXN][MAXN];//因為頂點數目較少並且陣列的讀取是O(1)所以定義成鄰接矩陣
int rmin=MAXNUM;//定義從外邊接線最小的代價
int vis[MAXN];//定義訪問標誌陣列
int sum;//定義最後的結果
void prim(int s)//呼叫普里母演算法構建最小生成樹,引數S表示從下標為S的點開始構建
{
    int low[MAXN];//定義每個點到其他點的最小權值
    for(int i=1; i<=n; i++)
    {
        low[i]=graph[s][i];//把每個點到其他點最小的權值點已成與s的距離,儘管可能不存在邊,但是我們初始化為maxnum了
    }
    vis[s]=1;//把s點新增到已經構建的頂點集合中
    for(int i=1; i<n; i++) //接下來的n-1次會找到n-1條最小的邊,恰好是一顆生成樹的邊數
    {
        int tmin=MAXNUM;//每次定義一個最小值,並且初始化為MAXNUM
        int tk=s;//並且標記最小權值的頂點下標
        for(int j=1; j<=n; j++)
        {
            if(vis[j]==0&&low[j]<tmin)//如果該店沒有被標記成已經構建好的頂點,並且比已知最小的還小
            {
                tmin=low[j];//更新最小值
                tk=j;//記錄該頂點下標
            }
        }
        vis[tk]=1;//把tk點新增到已經構建的頂點集合中
        sum+=tmin;//sum更新
        //接下來遍歷與tk點鄰接的所有點
        for(int j=1; j<=n; j++)
        {
            if(vis[j]==0&&low[j]>graph[tk][j])//如果tk下標的頂點與某個未被標記成已經構建點的直接邊的距離小於以前已經構建的頂點與該點的直接邊的權值
            {
                low[j]=graph[tk][j];//更新low陣列
            }
        }
    }
}
void init()//對於每個測試列初始化
{
    memset(vis,0,sizeof(vis));//初始化標記陣列為0
    memset(graph,0x3f,sizeof(graph));//初始化圖的所有點與其點的距離為MAX
    sum=0;//初始化sum和是0
    rmin=MAXNUM;//從外面接線的代價
}
int  main()
{
    int ncase;
    cin>>ncase;//輸入測試用例個數
    while(ncase--)
    {
        cin>>n>>e;
        init();//必要初始化
        for(int i=0; i<e; i++)
        {
            int v,w,d;
            cin>>v>>w>>d;
            graph[v][w]=d;
            graph[w][v]=d;
        }//構建樹
        for(int i=0; i<n; i++)
        {
            int num;
            cin>>num;
            if(num<rmin)
            {
                rmin=num;//更新最小的從外接入代價
            }
        }
        prim(1);//呼叫普里母演算法
        cout<<sum+rmin<<endl;//輸出結果,即最小的生成樹加外接最小代價
    }
    return 0;
}

如此說來我們就需要試一下克魯斯卡爾演算法了

首先分析下這個邊是v*(v-1)/2但是因為是結構體所以應該要比prim開的要大些

時間複雜度完全取決於庫函sort 對於n^2的結構體陣列的排序複雜度

#include<iostream>
#include<algorithm>//匯入演算法標頭檔案 需要庫函式sort
#define MAXN 124755//邊的最大數目
#define MAXNUM 0x3f3f3f3f//定義一個無窮大
typedef struct
{
    int v;
    int w;
    int data;
}Edge;//定義結構體-邊,即from  v,to w和邊的權值data  
Edge bian[MAXN];//定義邊的陣列
int minr;//定義從外接入的最小代價
int pre[MAXN];//並查集的父節點陣列
using namespace std;//匯入標準名稱空間
int n,e;
int sum;
int Find(int x)//壓縮路徑
{
    int temp,p=x;
    while(x!=pre[x])
    x=pre[x];//先找到x所屬集合的根節點
    while(p!=x)//更新路徑上的根節點全部壓縮
    {
        temp=pre[p];//從x到根節點路徑遍歷
        pre[p]=x;
        p=temp;
    }
    return x;//返回該集合的根節點
}
void kruskal()//克魯斯卡爾演算法進行最小生成樹的生成演算法
{
    for(int i=0;i<e;i++)//遍歷按照權值從小到大排好序的邊
    {
        if(Find(bian[i].v)!=Find(bian[i].w))//如果邊上兩個頂點或者說兩個點所在的圖不是一個,就是說所在的兩個圖是分離的不通的 ,這樣就規避了形成環的問題
        {
            pre[pre[bian[i].w]]=pre[bian[i].v];//那麼就把兩個點加入一個點集合
            sum+=bian[i].data;//更新sum
        }
    }
}
void init()//對於每個樣例初始化
{
    sum=0;//sum初始化為0
    minr=MAXNUM;//minr初始化為無窮大
    for(int i=1;i<=n;i++)//將父節點初始化為自己
        pre[i]=i;
}
bool cmp(Edge a,Edge b)//sort函式的引數函式
{
    return a.data<b.data;
}

int main()
{
    int ncase;
    cin>>ncase;//輸入樣例個數
    while(ncase--)
    {
        cin>>n>>e;
        init();//初始化
        for(int i=0;i<e;i++)
        {
            int w,v,d;
            cin>>v>>w>>d;
            bian[i].v=v;
            bian[i].w=w;
            bian[i].data=d;
        }//讀入到邊陣列中
        sort(bian,bian+e,cmp);//排好序
        for(int i=0;i<n;i++)
        {
            int num;
            cin>>num;
            if(num<minr)
                minr=num;
        }//更新從外面傳到內部電的最小代價
         kruskal();//呼叫克魯斯卡爾更新sum
         cout<<minr+sum<<endl;//輸出結果
    }
}

我們可以看下對比

prim :

kruskal:

我們可以看出在點相對比較少的情況下普里母完勝克魯斯卡爾

但是在邊少點多的情況下最好用克魯斯卡爾

並且在時間不夠用的情況下最好用克魯斯卡爾,應為演算法寫起來比較簡單