貪心法

貪心法(Greedy Approach)又稱貪婪法, 在對問題求解時,總是做出在當前看來是最好的選擇,或者說是:總是作出在當前看來最好的選擇。也就是說貪心算法並不從整體最優考慮,它所作出的選擇只是在某種意義上的局部最優選擇。當然,希望貪心算法得到的最終結果也是整體最優的。雖然貪心算法不能對所有問題都得到整體最優解,但對許多問題它能產生整體最優解。如單源最短路經問題,最小生成樹問題等。在一些情況下,即使貪心算法不能得到整體最優解,其最終結果卻是最優解的很好近似。


貪心法的設計思想

當一個問題具有以下的性質時可以用貪心算法求解:每一步的局部最優解,同事也說整個問題的最優解。

如果一個問題可以用貪心算法解決,那麼貪心通常是解決這個問題的最好的方法。 貪婪算法一般比其他方法例如動態規劃更有效。但是貪婪算法不能總是被應用。例如,部分揹包問題可以使用貪心解決,但是不能解決0-1揹包問題。

貪婪算法有時也用用來得到一個近似優化問題。例如,旅行商問題是一個NP難問題。貪婪選擇這個問題是選擇最近的並且從當前城市每一步。這個解決方案並不總是產生最好的最優解,但可以用來得到一個近似最優解。

讓我們考慮一下任務選擇的貪婪算法的問題, 作爲我們的第一個例子。問題:

給出n個任務和每個任務的開始和結束時間。找出可以完成的任務的最大數量,在同一時刻只能做一個任務。

例子:

下面的6個任務:
     start[]  =  {1, 3, 0, 5, 8, 5};
     finish[] =  {2, 4, 6, 7, 9, 9};
最多可完成的任務是:
 {0, 1, 3, 4}

貪婪的選擇是總是選擇下一個任務的完成時間至少在剩下的任務和開始時間大於或等於以前選擇任務的完成時間。我們可以根據他們的任務完成時間,以便我們總是認爲下一個任務是最小完成時間的任務。

  • 1)按照完成時間對任務排序
  • 2)選擇第一個任務排序數組元素和打印。
  • 3) 繼續以下剩餘的任務排序數組。

……a)如果這一任務的開始時間大於先前選擇任務的完成時間然後選擇這個任務和打印。

在接下來的C程序,假設已經根據任務的結束時間排序。

#include<stdio.h>
// 打印可以完成的最大數量的任務
//  n   -->  所有任務的數量
//  s[] -->  開始時間
//  f[] -->  結束時間
void printMaxActivities(int s[], int f[], int n)
{
    int i, j;
    printf ("Following activities are selected \n");
    // 選擇第一個任務
    i = 0;
    printf("%d ", i);
    //考慮剩下的任務
    for (j = 1; j < n; j++)
    {
      // 如果當前的任務開始比 前一個選擇的任務結束時間大或相等,就選擇它
      if (s[j] >= f[i])
      {
          printf ("%d ", j);
          i = j;
      }
    }
}

// driver program to test above function
int main()
{
    int s[] =  {1, 3, 0, 5, 8, 5};
    int f[] =  {2, 4, 6, 7, 9, 9};
    int n = sizeof(s)/sizeof(s[0]);
    printMaxActivities(s, f, n);
    getchar();
    return 0;
}

輸出:

Following activities are selected
0 1 3 4

貪心算法的基本要素

對於一個具體的問題,怎麼知道是否可用貪心算法解此問題,以及能否得到問題的最優解呢?這個問題很難給予肯定的回答。

但是,從許多可以用貪心算法求解的問題中看到這類問題一般具有2個重要的性質:貪心選擇性質和最優子結構性質。

  • 1、貪心選擇性質

所謂貪心選擇性質是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。

動態規劃算法通常以自底向上的方式解各子問題,而貪心算法則通常以自頂向下的方式進行,以迭代的方式作出相繼的貪心選擇,每作一次貪心選擇就將所求問題簡化爲規模更小的子問題。

對於一個具體問題,要確定它是否具有貪心選擇性質,必須證明每一步所作的貪心選擇最終導致問題的整體最優解。

  • 2、最優子結構性質

當一個問題的最優解包含其子問題的最優解時,稱此問題具有最優子結構性質。問題的最優子結構性質是該問題可用動態規劃算法或貪心算法求解的關鍵特徵。

  • 3、貪心算法與動態規劃算法的差異

貪心算法和動態規劃算法都要求問題具有最優子結構性質,這是2類算法的一個共同點。但是,對於具有最優子結構的問題應該選用貪心算法還是動態規劃算法求解?是否能用動態規劃算法求解的問題也能用貪心算法求解?下面研究2個經典的組合優化問題,並以此說明貪心算法與動態規劃算法的主要差別。

0-1揹包問題:
給定n種物品和一個揹包。物品i的重量是Wi,其價值爲Vi,揹包的容量爲C。應如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?

在選擇裝入揹包的物品時,對每種物品i只有2種選擇,即裝入揹包或不裝入揹包。不能將物品i裝入揹包多次,也不能只裝入部分的物品i。

揹包問題:
與0-1揹包問題類似,所不同的是在選擇物品i裝入揹包時,可以選擇物品i的一部分,而不一定要全部裝入揹包,1 <= i <= n。

這2類問題都具有最優子結構性質,極爲相似,但揹包問題可以用貪心算法求解,而0-1揹包問題卻不能用貪心算法求解。

用貪心算法解揹包問題的基本步驟:

  • 首先計算每種物品單位重量的價值Vi/Wi
  • 然後,依貪心選擇策略,將儘可能多的單位重量價值最高的物品裝入揹包。
  • 若將這種物品全部裝入揹包後,揹包內的物品總重量未超過C,則選擇單位重量價值次高的物品並儘可能多地裝入揹包。
  • 依此策略一直地進行下去,直到揹包裝滿爲止。

僞代碼:

void Knapsack(int n,float M,float v[],float w[],float x[])
{
  Sort(n,v,w);
  int i;
  for (i = 1 ; i <= n ; i++) 
    x[i] = 0;
    float c=M;
    for (i=1;i<=n;i++) {
      if (w[i] > c) break;
    }
    x[i]=1;
    c-=w[i];
  }
  if (i <= n) 
    x[i]=c / w[i];
}

算法knapsack的主要計算時間在於將各種物品依其單位重量的價值從大到小排序。因此,算法的計算時間上界爲 O(nlogn)。

爲了證明算法的正確性,還必須證明揹包問題具有貪心選擇性質。

對於0-1揹包問題,貪心選擇之所以不能得到最優解是因爲在這種情況下,它無法保證最終能將揹包裝滿,部分閒置的揹包空間使每公斤揹包空間的價值降低了。事實上,在考慮0-1揹包問題時,應比較選擇該物品和不選擇該物品所導致的最終方案,然後再作出最好選擇。由此就導出許多互相重疊的子問題。這正是該問題可用動態規劃算法求解的另一重要特徵。實際上也是如此,動態規劃算法的確可以有效地解0-1揹包問題。


貪心法的典型應用

活動安排問題

問題描述:設有n 個活動的集合E={1,2,,n} ,其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每個活i 都有一個要求使用該資源的起始時間si 和一個結束時間fi ,且si<fi 。如果選擇了活動i,則它在半開時間區間[si,fi) 內佔用資源。若區間[si,fi) 與區間[sj,fj) 不相交,則稱活動i 與活動j 是相容的。也就是說,當si>=fjsj>=fi 時,活動i 與活動j 相容。

由於輸入的活動以其完成時間的非減序排列,所以算法 greedySelector每次總是選擇具有最早完成時間的相容活動加入集合A中。直觀上,按這種方法選擇相容活動爲未安排活動留下儘可能多的時間。也就是說,該算法的貪心選擇的意義是使剩餘的可安排時間段極大化,以便安排儘可能多的相容活動。

算法greedySelector 的效率極高。當輸入的活動已按結束時間的非減序排列,算法只需O(n) 的時間安排n個活動,使最多的活動能相容地使用公共資源。如果所給出的活動未按非減序排列,可以用O(nlogn) 的時間重排。

例:設待安排的11個活動的開始時間和結束時間按結束時間的非減序排列如下:

i 1 2 3 4 5 6 7 8 9 10 11
S[i] 1 3 0 5 3 5 6 8 8 2 12
f[i] 4 5 6 7 8 9 10 11 12 13 14

算法greedySelector 的計算過程如下圖所示[圖來源網絡]。圖中每行相應於算法的一次迭代。陰影長條表示的活動是已選入集合A的活動,而空白長條表示的活動是當前正在檢查相容性的活動。
這裏寫圖片描述

若被檢查的活動i的開始時間Si 小於最近選擇的活動j 的結束時間fi ,則不選擇活動i ,否則選擇活動i 加入集合A 中。

貪心算法並不總能求得問題的整體最優解。但對於活動安排問題,貪心算法greedySelector 卻總能求得的整體最優解,即它最終所確定的相容活動集合A的規模最大。這個結論可以用數學歸納法證明。

活動安排問題實現:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;

struct ActivityTime
{
public:
    ActivityTime (int nStart, int nEnd) 
        : m_nStart (nStart), m_nEnd (nEnd) 
    { }
    ActivityTime ()
        : m_nStart (0), m_nEnd (0)
    { }
    friend 
    bool operator < (const ActivityTime& lth, const ActivityTime& rth) 
    {
        return lth.m_nEnd < lth.m_nEnd ;
    }
public:
    int m_nStart ;
    int m_nEnd ;
} ;

class ActivityArrange 
{
public:
    ActivityArrange (const vector<ActivityTime>& vTimeList) 
    {
        m_vTimeList = vTimeList ;
        m_nCount = vTimeList.size () ;
        m_bvSelectFlag.resize (m_nCount, false) ;
    }
    // 活動安排
    void greedySelector () 
    {
        __sortTime () ;
        // 第一個活動一定入內
        m_bvSelectFlag[0] = true ;    
        int j = 0 ;
        for (int i = 1; i < m_nCount ; ++ i) {
            if (m_vTimeList[i].m_nStart > m_vTimeList[j].m_nEnd) {
                m_bvSelectFlag[i] = true ;
                j = i ;
            }
        }

        copy (m_bvSelectFlag.begin(), m_bvSelectFlag.end() ,ostream_iterator<bool> (cout, ” “));
        cout << endl ;
    }

private:
    // 按照活動結束時間非遞減排序
    void __sortTime () 
    {
        sort (m_vTimeList.begin(), m_vTimeList.end()) ;
        for (vector<ActivityTime>::iterator ite = m_vTimeList.begin() ;
                ite != m_vTimeList.end() ; 
                ++ ite) {
            cout << ite->m_nStart << “, “<< ite ->m_nEnd << endl ;
        }
    }

private:
    vector<ActivityTime>    m_vTimeList ;    // 活動時間安排列表
    vector<bool>            m_bvSelectFlag ;// 是否安排活動標誌
    int    m_nCount ;    // 總活動個數
} ;

int main()
{
    vector<ActivityTime> vATimeList ;
    vATimeList.push_back (ActivityTime(1, 4)) ;
    vATimeList.push_back (ActivityTime(3, 5)) ;
    vATimeList.push_back (ActivityTime(0, 6)) ;
    vATimeList.push_back (ActivityTime(5, 7)) ;
    vATimeList.push_back (ActivityTime(3, 8)) ;
    vATimeList.push_back (ActivityTime(5, 9)) ;
    vATimeList.push_back (ActivityTime(6, 10)) ;
    vATimeList.push_back (ActivityTime(8, 11)) ;
    vActiTimeList.push_back (ActivityTime(8, 12)) ;
    vATimeList.push_back (ActivityTime(2, 13)) ;
    vATimeList.push_back (ActivityTime(12, 14)) ;

    ActivityArrange aa (vATimeList) ;
    aa.greedySelector () ;
    return 0 ;
}

最優前綴碼

Huffman 編碼是一種無損壓縮技術。它分配可變長度編碼不同的字符。貪婪的選擇是分配一點代碼最常見的字符長度。哈夫曼編碼是廣泛地用於數據文件壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。哈夫曼編碼算法用字符在文件中出現的頻率表來建立一個用0,1串表示各字符的最優表示方式。

給出現頻率高的字符較短的編碼,出現頻率較低的字符以較長的編碼,可以大大縮短總碼長。

_ a b c d e f
頻率(千次) 45 13 12 16 9 5
定長碼 000 001 010 011 100 101
變長碼 0 101 100 111 1101 1100

定長碼:3(45+13+12+16+9+5)=300 千位
變長碼:145+313+312+316+49+45224 千位

1、前綴碼

對每一個字符規定一個0,1串作爲其代碼,並要求任一字符的代碼都不是其它字符代碼的前綴。這種編碼稱爲前綴碼。

編碼的前綴性質可以使譯碼方法非常簡單。

表示最優前綴碼的二叉樹總是一棵完全二叉樹,即樹中任一結點都有2個兒子結點。

平均碼長定義爲:

B(T)=cCf(c)dT(c)

其中,f(c) 表示字符c出現的概率,dt(c) 表示c的碼長.使平均碼長達到最小的前綴碼編碼方案稱爲給定編碼字符集C的最優前綴碼。

2、構造哈夫曼編碼

哈夫曼提出構造最優前綴碼的貪心算法,由此產生的編碼方案稱爲哈夫曼編碼。哈夫曼算法以自底向上的方式構造表示最優前綴碼的二叉樹T。

算法以|C| 個葉結點開始,執行|C|1 次的“合併”運算後產生最終所要求的樹T

f 爲鍵值的優先隊列Q 用在貪心選擇時有效地確定算法當前要合併的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合併後,產生一棵新的樹,其頻率爲合併的2棵樹的頻率之和,並將新樹插入優先隊列Q。經過n1 次的合併後,優先隊列中只剩下一棵樹,即所要求的樹T。

算法huffmanTree 用最小堆實現優先隊列Q。初始化優先隊列需要O(n) 計算時間,由於最小堆的removeMinput 運算均需O(logn) 時間,n1 次的合併總共需要O(nlogn) 計算時間。因此,關於n 個字符的哈夫曼算法的計算時間爲O(nlogn)

3、哈夫曼算法的正確性

要證明哈夫曼算法的正確性,只要證明最優前綴碼問題具有貪心選擇性質和最優子結構性質。

  • (1)貪心選擇性質
  • (2)最優子結構性質

實現代碼(Code highlighting produced by Actipro CodeHighlighter )

#include <iostream>
#include <vector>
#include <queue> 
using namespace std ;


class HaffmanNode
{
public:
    HaffmanNode (int nKeyValue, 
                HaffmanNode* pLeft = NULL,
                HaffmanNode* pRight = NULL)
    { 
        m_nKeyValue = nKeyValue ;
        m_pLeft = pLeft ;
        m_pRight = pRight ;
    }

    friend 
    bool operator < (const HaffmanNode& lth, const HaffmanNode& rth)
    {
        return lth.m_nKeyValue < rth.m_nKeyValue ;
    }

public:
    int        m_nKeyValue ;    
    HaffmanNode*    m_pLeft ;
    HaffmanNode*    m_pRight ;
} ;

class HaffmanCoding
{
public:
    typedef priority_queue<HaffmanNode*> MinHeap ;
    typedef HaffmanNode*    HaffmanTree ;

public:
    HaffmanCoding (const vector<int>& weight) 
        : m_pTree(NULL)
    {
        m_stCount = weight.size () ;
        for (size_t i = 0; i < weight.size() ; ++ i) {
            m_minheap.push (new HaffmanNode(weight[i], NULL, NULL)) ;
        }
    }
    ~ HaffmanCoding()
    {
        __destroy (m_pTree) ;
    }

    // 按照左1右0編碼 
    void doHaffmanCoding ()
    {
        vector<int> vnCode(m_stCount-1) ;
        __constructTree () ;
        __traverse (m_pTree, 0, vnCode) ;
    }

private:
    void __destroy(HaffmanTree& ht) 
    {
        if (ht->m_pLeft != NULL) {
            __destroy (ht->m_pLeft) ;
        }
        if (ht->m_pRight != NULL) {
            __destroy (ht->m_pRight) ;
        }
        if (ht->m_pLeft == NULL && ht->m_pRight == NULL) {
            // cout << "delete" << endl ;
            delete ht ;
            ht = NULL ;
        }
    }
    void __traverse (HaffmanTree ht,int layers, vector<int>& vnCode) 
    {
        if (ht->m_pLeft != NULL) {
            vnCode[layers] = 1 ;
            __traverse (ht->m_pLeft, ++ layers, vnCode) ;
            -- layers ;
        }
        if (ht->m_pRight != NULL) {
            vnCode[layers] = 0 ;
            __traverse (ht->m_pRight, ++ layers, vnCode) ;
            -- layers ;
        }
        if (ht->m_pLeft == NULL && ht->m_pRight == NULL) {
            cout << ht->m_nKeyValue << " coding:  " ;
            for (int i = 0; i < layers; ++ i) {
                 cout << vnCode[i] << " " ;
            }
            cout << endl ;
        }
    }

    void __constructTree ()
    {
        size_t i = 1 ;
        while (i < m_stCount) {
            HaffmanNode* lchild = m_minheap.top () ;
            m_minheap.pop () ;
            HaffmanNode* rchild = m_minheap.top () ;
            m_minheap.pop () ;

            // 確保左子樹的鍵值大於有子樹的鍵值
            if (lchild->m_nKeyValue < rchild->m_nKeyValue) {
                HaffmanNode* temp = lchild ;
                lchild = rchild ;
                rchild = temp ;
            }
            // 構造新結點
            HaffmanNode* pNewNode = 
                new HaffmanNode (lchild->m_nKeyValue + rchild->m_nKeyValue,
                lchild, rchild ) ;
            m_minheap.push (pNewNode) ;
            ++ i ;
        }
        m_pTree = m_minheap.top () ;
        m_minheap.pop () ;
    }

private:
    vector<int> m_vnWeight ;    // 權值
    HaffmanTree m_pTree ;
    MinHeap        m_minheap ;
    size_t        m_stCount ;        // 葉結點個數
} ;


int main()
{    
    vector<int> vnWeight ;
    vnWeight.push_back (45) ;
    vnWeight.push_back (13) ;
    vnWeight.push_back (12) ;
    vnWeight.push_back (16) ;
    vnWeight.push_back (9) ;
    vnWeight.push_back (5) ;

    HaffmanCoding hc (vnWeight) ;
    hc.doHaffmanCoding () ;
    return 0 ;
}

單源最短路徑

給定帶權有向圖G=(V,E) ,其中每條邊的權是非負實數。另外,還給定V 中的一個頂點,稱爲源。現在要計算從源到所有其它各頂點的最短路長度。這裏路的長度是指路上各邊權之和。這個問題通常稱爲單源最短路徑問題。

1、算法基本思想

Dijkstra算法是解單源最短路徑問題的貪心算法。

其基本思想是,設置頂點集合S並不斷地作貪心選擇來擴充這個集合。一個頂點屬於集合S當且僅當從源到該頂點的最短路徑長度已知。

初始時,S中僅含有源。設u是G的某一個頂點,把從源到u且中間只經過S中頂點的路稱爲從源到u的特殊路徑,並用數組dist記錄當前每個頂點所對應的最短特殊路徑長度。Dijkstra算法每次從V-S中取出具有最短特殊路長度的頂點u,將u添加到S中,同時對數組dist作必要的修改。一旦S包含了所有V中頂點,dist就記錄了從源到所有其它頂點之間的最短路徑長度。

例如,對下圖中的有向圖,應用Dijkstra算法計算從源頂點1到其它頂點間最短路徑的過程列在下表中。

這裏寫圖片描述

Dijkstra算法的迭代過程:

迭代 s u dist[2] dist[3] dist[4] dist[5]
初始 {1} - 10 maxint 30 100
1 {1,2} 2 10 60 30 100
2 {1,2,4} 4 10 50 30 90
3 {1,2,4,3} 3 10 50 30 60
4 {1,2,4,3,5} 5 10 50 30 60

2、算法的正確性和計算複雜性

(1)貪心選擇性質
(2)最優子結構性質
(3)計算複雜性

對於具有n個頂點和e條邊的帶權有向圖,如果用帶權鄰接矩陣表示這個圖,那麼Dijkstra 算法的主循環體需要O(n) 時間。這個循環需要執行n1 次,所以完成循環需要O(n) 時間。算法的其餘部分所需要時間不超過O(n2)

實現:

#include <iostream>
#include <vector>
#include <limits>
using namespace std ;

class BBShortestDijkstra
{
public:
    BBShortestDijkstra (const vector<vector<int> >& vnGraph) 
        :m_cnMaxInt (numeric_limits<int>::max()) 
    {
        m_vnGraph = vnGraph ;
        m_stCount = vnGraph.size () ;
        m_vnDist.resize (m_stCount) ;
        for (size_t i = 0; i < m_stCount; ++ i) {
            m_vnDist[i].resize (m_stCount) ;
        }
    }

    void doDijkatra ()
    {
        int nMinIndex = 0 ;
        int nMinValue = m_cnMaxInt ;
        vector<bool> vbFlag (m_stCount, false) ;
        for (size_t i = 0; i < m_stCount; ++ i) {
            m_vnDist[0][i] = m_vnGraph[0][i] ;
            if (nMinValue > m_vnGraph[0][i]) {
                nMinValue = m_vnGraph[0][i] ;
                nMinIndex = i ;
            }
        }

        vbFlag[0] = true ;
        size_t k = 1 ;
        while (k < m_stCount) {
            vbFlag[nMinIndex] = true ;
            for (size_t j = 0; j < m_stCount ; ++ j) {
                // 沒有被選擇
                if (!vbFlag[j] && m_vnGraph[nMinIndex][j] != m_cnMaxInt ) {
                    if (m_vnGraph[nMinIndex][j] + nMinValue
                        < m_vnDist[k-1][j]) {
                        m_vnDist[k][j] = m_vnGraph[nMinIndex][j] + nMinValue ;
                    }
                    else {
                        m_vnDist[k][j] = m_vnDist[k-1][j] ;
                    }
                }
                else {
                    m_vnDist[k][j] = m_vnDist[k-1][j] ;
                }
            }
            nMinValue = m_cnMaxInt ;
            for (size_t j = 0; j < m_stCount; ++ j) {
                if (!vbFlag[j] && (nMinValue > m_vnDist[k][j])) {
                    nMinValue = m_vnDist[k][j] ;
                    nMinIndex = j ;
                }
            }
            ++ k ;
        }

        for (int i = 0; i < m_stCount; ++ i) {
            for (int j = 0; j < m_stCount; ++ j) {
                if (m_vnDist[i][j] == m_cnMaxInt) {
                    cout << "maxint " ;
                }
                else {
                    cout << m_vnDist[i][j] << " " ;
                }
            }
            cout << endl ;
        }
    }
private:  
    vector<vector<int> >    m_vnGraph ;
    vector<vector<int> >    m_vnDist ;
    size_t m_stCount ;
    const int m_cnMaxInt ;
} ;

int main()
{
    const int cnCount = 5 ;
    vector<vector<int> > vnGraph (cnCount) ;
    for (int i = 0; i < cnCount; ++ i) {
        vnGraph[i].resize (cnCount, numeric_limits<int>::max()) ;
    }
    vnGraph[0][1] = 10 ;
    vnGraph[0][3] = 30 ;
    vnGraph[0][4] = 100 ;
    vnGraph[1][2] = 50 ;
    vnGraph[2][4] = 10 ;
    vnGraph[3][2] = 20 ;
    vnGraph[3][4] = 60 ;

    BBShortestDijkstra bbs (vnGraph) ;
    bbs.doDijkatra () ;
}

最小生成樹

G=(V,E) 是無向連通帶權圖,即一個網絡。E中的每一條邊v,w 的權爲c[v][w] 。如果G的子圖G’是一棵包含G的所有頂點的樹,則稱G’爲G的生成樹。生成樹上各邊權的總和稱爲生成樹的耗費。在G的所有生成樹中,耗費最小的生成樹稱爲G的最小生成樹。最小生成樹的性質:設G = (V,E)是連通帶權圖,U是V的真子集。如果(u,v)∈E,且u∈U,v∈V-U,且在所有這樣的邊中,(u,v)的權c[u][v]最小,那麼一定存在G的一棵最小生成樹,它意(u,v)爲其中一條邊。這個性質有時也稱爲MST性質。

構造最小生成樹的兩種方法:Prim算法和Kruskal算法。Kruskal最小生成樹(MST):在Kruskal算法中,我們通過逐個的選取最優邊來獲得一個MST。每次選擇最小權重並且不構成環的邊。Prim小生成樹算法:在prim算法中,我們也是逐個的選取最優邊來獲得一個MST。我們維持兩組:已經包含在MST的頂點和頂點的集合不包括在內的。貪婪的選擇是選擇最小的重量邊緣連接兩組。

Prim算法

設G = (V,E)是連通帶權圖,V = {1,2,…,n}。構造G的最小生成樹Prim算法的基本思想是:首先置S = {1},然後,只要S是V的真子集,就進行如下的貪心選擇:選取滿足條件i ∈S,j ∈V – S,且c[i][j]最小的邊,將頂點j添加到S中。這個過程一直進行到S = V時爲止。在這個過程中選取到的所有邊恰好構成G的一棵最小生成樹。

如下帶權圖:
這裏寫圖片描述

生成過程:

1 -> 3 : 1
3 -> 6 : 4
6 -> 4: 2
3 -> 2 : 5
2 -> 5 : 3

實現:

/* 最小生成樹(Prim)*/

#include <iostream>
#include <vector>
#include <limits>
using namespace std ;

struct TreeNode
{
public:
    TreeNode (int nVertexIndexA = 0, int nVertexIndexB = 0, int nWeight = 0)
        : m_nVertexIndexA (nVertexIndexA),
        m_nVertexIndexB (nVertexIndexB),
        m_nWeight (nWeight)
    { }
public:
    int m_nVertexIndexA ;
    int m_nVertexIndexB ;
    int m_nWeight ;
} ;

class MST_Prim
{
public:
    MST_Prim (const vector<vector<int> >& vnGraph) 
    {
        m_nvGraph = vnGraph ;
        m_nNodeCount = (int)m_nvGraph.size () ;
    }
    void DoPrim ()
    {
        // 是否被訪問標誌
        vector<bool> bFlag (m_nNodeCount, false) ;
        bFlag[0] = true ;

        int nMaxIndexA ;
        int nMaxIndexB ;
        int j = 0 ;
        while (j < m_nNodeCount - 1) {
            int nMaxWeight = numeric_limits<int>::max () ;
            // 找到當前最短路徑
            int i = 0 ;
            while (i < m_nNodeCount) {
                if (!bFlag[i]) {
                    ++ i ;
                    continue ;
                }
                for (int j = 0; j < m_nNodeCount; ++ j) {
                    if (!bFlag[j] && nMaxWeight > m_nvGraph[i][j]) {
                        nMaxWeight = m_nvGraph[i][j] ;
                        nMaxIndexA = i ;
                        nMaxIndexB = j ;
                    } 
                }
                ++ i ;
            }
            bFlag[nMaxIndexB] = true ;
            m_tnMSTree.push_back (TreeNode(nMaxIndexA, nMaxIndexB, nMaxWeight)) ;
            ++ j ;
        }
        // 輸出結果
        for (vector<TreeNode>::const_iterator ite = m_tnMSTree.begin() ;
                ite != m_tnMSTree.end() ;
                ++ ite ) {
            cout << (*ite).m_nVertexIndexA << "->" 
                << (*ite).m_nVertexIndexB << " : "
                << (*ite).m_nWeight << endl ;
        }
    }
private:
    vector<vector<int> > m_nvGraph ;    // 無向連通圖
    vector<TreeNode>    m_tnMSTree ;    // 最小生成樹
    int    m_nNodeCount ;
} ;

int main()
{
    const int cnNodeCount = 6 ;
    vector<vector<int> > graph (cnNodeCount) ;
    for (size_t i = 0; i < graph.size() ; ++ i) {
        graph[i].resize (cnNodeCount, numeric_limits<int>::max()) ;
    }
    graph[0][1]= 6 ;
    graph[0][2] = 1 ;
    graph[0][3] = 5 ;
    graph[1][2] = 5 ;
    graph[1][4] = 3 ;
    graph[2][3] = 5 ;
    graph[2][4] = 6 ;
    graph[2][5] = 4 ;
    graph[3][5] = 2 ;
    graph[4][5] = 6 ;

    graph[1][0]= 6 ;
    graph[2][0] = 1 ;
    graph[3][0] = 5 ;
    graph[2][1] = 5 ;
    graph[4][1] = 3 ;
    graph[3][2] = 5 ;
    graph[4][2] = 6 ;
    graph[5][2] = 4 ;
    graph[5][3] = 2 ;
    graph[5][4] = 6 ;

    MST_Prim mstp (graph) ;
    mstp.DoPrim () ;
     return 0 ;
}

Kruskal算法

當圖的邊數爲e時,Kruskal算法所需的時間是O(eloge)。當e=Ω(n2) 時,Kruskal算法比Prim算法差;但當e=o(n2) 時,Kruskal算法比Prim算法好得多。給定無向連同帶權圖G=(V,E) ,V={1,2,,n} 。Kruskal算法構造G的最小生成樹的基本思想是:

(1)首先將G的n個頂點看成n個孤立的連通分支。將所有的邊按權從小大排序。
(2)從第一條邊開始,依邊權遞增的順序檢查每一條邊。並按照下述方法連接兩個不同的連通分支:當查看到第k條邊(v,w)時,如果端點v和w分別是當前兩個不同的連通分支T1和T2的端點是,就用邊(v,w)將T1和T2連接成一個連通分支,然後繼續查看第k+1條邊;如果端點v和w在當前的同一個連通分支中,就直接再查看k+1條邊。這個過程一個進行到只剩下一個連通分支時爲止。

此時,已構成G的一棵最小生成樹。

Kruskal算法的選邊過程:

1 -> 3 : 1
4 -> 6 : 2
2 -> 5 : 3
3 -> 4 : 4
2 -> 3 : 5

實現:

/* Kruskal算法)*/

#include <iostream>
#include <vector>
#include <queue>
#include <limits>
using namespace std ;

struct TreeNode
{
public:
    TreeNode (int nVertexIndexA = 0, int nVertexIndexB = 0, int nWeight = 0)
        : m_nVertexIndexA (nVertexIndexA),
        m_nVertexIndexB (nVertexIndexB),
        m_nWeight (nWeight)
    { }
    friend 
    bool operator < (const TreeNode& lth, const TreeNode& rth) 
    {
        return lth.m_nWeight > rth.m_nWeight ;
    }

public:
    int m_nVertexIndexA ;
    int m_nVertexIndexB ;
    int m_nWeight ;
} ;

//  並查集
class UnionSet 
{
public:
    UnionSet (int nSetEleCount)
        : m_nSetEleCount (nSetEleCount)
    {
        __init() ;
    }
    // 合併i,j。如果i,j同在集合中,返回false。否則返回true
    bool Union (int i, int j)
    {
        int ifather = __find (i) ;
        int jfather = __find (j) ;
        if (ifather == jfather )
        {
            return false ;
            // copy (m_nvFather.begin(), m_nvFather.end(), ostream_iterator<int> (cout, " "));
            // cout << endl ;
        }
        else
        {
            m_nvFather[jfather] = ifather ;
            // copy (m_nvFather.begin(), m_nvFather.end(), ostream_iterator<int> (cout, " "));
            // cout << endl ;
            return true ;
        }

    }

private:
    // 初始化並查集
    int __init()
    {
        m_nvFather.resize (m_nSetEleCount) ;
        for (vector<int>::size_type i = 0 ;
            i < m_nSetEleCount;
            ++ i )
        {
            m_nvFather[i] = static_cast<int>(i) ;
            // cout << m_nvFather[i] << " " ;
        }
        // cout << endl ;
        return 0 ;
    }
    // 查找index元素的父親節點 並且壓縮路徑長度 
    int __find (int nIndex)
    {
        if (nIndex == m_nvFather[nIndex])
        {
            return nIndex;
        }
        return  m_nvFather[nIndex] = __find (m_nvFather[nIndex]);
    }

private:
    vector<int>                m_nvFather ;    // 父親數組
    vector<int>::size_type m_nSetEleCount ;    // 集合中結點個數
} ;

class MST_Kruskal
{
    typedef priority_queue<TreeNode> MinHeap ;
public:
    MST_Kruskal (const vector<vector<int> >& graph) 
    {
        m_nNodeCount = static_cast<int>(graph.size ()) ;
        __getMinHeap (graph) ;
    }
    void DoKruskal ()
    {
        UnionSet us (m_nNodeCount) ;
        int k = 0 ; 
        while (m_minheap.size() != 0 && k < m_nNodeCount - 1) 
        {
            TreeNode tn = m_minheap.top () ;
            m_minheap.pop () ;
            // 判斷合理性
            if (us.Union (tn.m_nVertexIndexA, tn.m_nVertexIndexB)) 
            {
                m_tnMSTree.push_back (tn) ;
                ++ k ;
            }
        }
        // 輸出結果
        for (size_t i = 0; i < m_tnMSTree.size() ; ++ i) 
        {
            cout << m_tnMSTree[i].m_nVertexIndexA << "->"
                << m_tnMSTree[i].m_nVertexIndexB << " : "
                << m_tnMSTree[i].m_nWeight 
                << endl ;
        }
    }

private:
    void __getMinHeap (const vector<vector<int> >& graph) 
    {
        for (int i = 0; i < m_nNodeCount; ++ i) 
        {
            for (int j = 0; j < m_nNodeCount; ++ j) 
            {
                if (graph[i][j] != numeric_limits<int>::max()) 
                {
                    m_minheap.push (TreeNode(i, j, graph[i][j])) ;
                }
            }
        }
    }
private:
    vector<TreeNode>    m_tnMSTree ;
    int                    m_nNodeCount ;
    MinHeap                m_minheap ;
} ;


int main ()
{
    const int cnNodeCount = 6 ;
    vector<vector<int> > graph (cnNodeCount) ;
    for (size_t i = 0; i < graph.size() ; ++ i) 
    {
        graph[i].resize (cnNodeCount, numeric_limits<int>::max()) ;
    }
    graph[0][1]= 6 ;
    graph[0][2] = 1 ;
    graph[0][3] = 3 ;
    graph[1][2] = 5 ;
    graph[1][4] = 3 ;
    graph[2][3] = 5 ;
    graph[2][4] = 6 ;
    graph[2][5] = 4 ;
    graph[3][5] = 2 ;
    graph[4][5] = 6 ;

    graph[1][0]= 6 ;
    graph[2][0] = 1 ;
    graph[3][0] = 3 ;
    graph[2][1] = 5 ;
    graph[4][1] = 3 ;
    graph[3][2] = 5 ;
    graph[4][2] = 6 ;
    graph[5][2] = 4 ;
    graph[5][3] = 2 ;
    graph[5][4] = 6 ;

    MST_Kruskal mst (graph);
    mst.DoKruskal () ;
}

參考資料

  1. Donald E.Knuth 著,蘇運霖 譯,《計算機程序設計藝術,第1卷基本算法》,國防工業出版社,2002年
  2. Donald E.Knuth 著,蘇運霖 譯,《計算機程序設計藝術,第2卷半數值算法》,國防工業出版社,2002年
  3. Donald E.Knuth 著,蘇運霖 譯,《計算機程序設計藝術,第3卷排序與查找》,國防工業出版社,2002年
  4. Thomas H. Cormen, Charles E.Leiserson, etc., Introduction to Algorithms(3rd edition), McGraw-Hill Book Company,2009
  5. Jon Kleinberg, Ėva Tardos, Algorithm Design, Addison Wesley, 2005.
  6. Sartaj Sahni ,《數據結構算法與應用:C++語言描述》 ,汪詩林等譯,機械工業出版社,2000.
  7. 屈婉玲,劉田,張立昂,王捍貧,算法設計與分析,清華大學出版社,2011年版,2013年重印.
  8. 張銘,趙海燕,王騰蛟,《數據結構與算法實驗教程》,高等教育出版社,2011年 1月
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章