貪心算法

原文鏈接:http://blog.csdn.net/effective_coder/article/details/8736718生氣



有人說貪心算法是最簡單的算法,原因很簡單:你我其實都很貪,根本不用學就知道怎麼貪。有人說貪心算法是最複雜的算法,原因也很簡單:這世上會貪的人太多了,那輪到你我的份?

 

 

                                                                                                   貪心算法詳解

 

       

貪心算法思想:

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

貪心算法的基本要素:

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

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

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

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

貪心算法的基本思路:

從問題的某一個初始解出發逐步逼近給定的目標,以儘可能快的地求得更好的解。當達到算法中的某一步不能再繼續前進時,算法停止。

該算法存在問題:

1. 不能保證求得的最後解是最佳的;

2. 不能用來求最大或最小解問題;

3. 只能求滿足某些約束條件的可行解的範圍。

實現該算法的過程:

從問題的某一初始解出發;

while 能朝給定總目標前進一步 do

   求出可行解的一個解元素;

由所有解元素組合成問題的一個可行解;

用揹包問題來介紹貪心算法:

揹包問題:有一個揹包,揹包容量是M=150。有7個物品,物品可以分割成任意大小。要求儘可能讓裝入揹包中的物品總價值最大,但不能超過總容量。

物品 A B C D E F G

重量 35 30 60 50 40 10 25

價值 10 40 30 50 35 40 30

分析如下

目標函數: ∑pi最大

約束條件是裝入的物品總重量不超過揹包容量:∑wi<=M( M=150)。

(1)根據貪心的策略,每次挑選價值最大的物品裝入揹包,得到的結果是否最優?

(2)每次挑選所佔重量最小的物品裝入是否能得到最優解?

(3)每次選取單位重量價值最大的物品,成爲解本題的策略。

值得注意的是,貪心算法並不是完全不可以使用,貪心策略一旦經過證明成立後,它就是一種高效的算法。

貪心算法還是很常見的算法之一,這是由於它簡單易行,構造貪心策略不是很困難。

可惜的是,它需要證明後才能真正運用到題目的算法中。

一般來說,貪心算法的證明圍繞着:整個問題的最優解一定由在貪心策略中存在的子問題的最優解得來的。

對於揹包問題中的3種貪心策略,都是無法成立(無法被證明)的,解釋如下:

貪心策略:選取價值最大者。反例:

W=30

物品:A B C

重量:28 12 12

價值:30 20 20

根據策略,首先選取物品A,接下來就無法再選取了,可是,選取B、C則更好。

(2)貪心策略:選取重量最小。它的反例與第一種策略的反例差不多。

(3)貪心策略:選取單位重量價值最大的物品。反例:

W=30

物品:A B C

重量:28 20 10

價值:28 20 10

根據策略,三種物品單位重量價值一樣,程序無法依據現有策略作出判斷,如果選擇A,則答案錯誤。但是果在條件中加一句當遇見單位價值相同的時候,優先裝重量小的,這樣的問題就可以解決.

所以需要說明的是,貪心算法可以與隨機化算法一起使用,具體的例子就不再多舉了。(因爲這一類算法普及性不高,而且技術含量是非常高的,需要通過一些反例確定隨機的對象是什麼,隨機程度如何,但也是不能保證完全正確,只能是極大的機率正確)。

 

網上對於這個裝包問題的描述就就只有這些,但是在這裏我還是要寫一下,假設條件是什麼?假設條件是上述幾種反例的情況不存在的時候該如何求解:

#include <iostream>  
using namespace std;  
  
struct Node  
{  
    float weight;  
    float value;  
    bool mark;  
    char char_mark;  
    float pre_weight_value;  
};  
  
int main(int argc, char* argv[])  
{  
    float Weight[7] = {35,30,60,50,40,15,20};  
    float Value [7] = {10,40,30,50,35,40,30};  
    Node array[7];  
    for(int i=0; i<7; i++)  
    {  
        array[i].value = Value[i];  
        array[i].weight = Weight[i];  
        array[i].char_mark = 65 + i;  
        array[i].mark = false;  
        array[i].pre_weight_value = Value[i] / Weight[i];  
    }  
      
    for(i=0;i<7;i++)  
        cout<<array[i].pre_weight_value<<" ";  
    cout<<endl;  
      
    float weight_all=0.0;  
    float value_all = 0.0;  
    float max = 0.0;  
    char charArray[7];  
    int flag,n = 0;  
      
    while(weight_all <= 150)  
    {  
        for(int index=0;index < 7; ++index)  
        {  
            if(array[index].pre_weight_value > max && array[index].mark == false)  
            {  
                max = array[index].pre_weight_value ;  
                flag = index;  
            }  
        }  
          
        charArray[n++] = array[flag].char_mark;  
        array[flag].mark = true;  
        weight_all += array[flag].weight;  
        value_all += array[flag].value;  
        max = 0.0;  
    }  
      
    for(i=0;i<n-1;i++)  
        cout<<charArray[i]<<" ";  
    cout<<endl;  
    cout<<"weight_all:"<<weight_all- array[n-1].weight<<endl;  
    cout<<"value_all:"<<value_all<<endl;  
      
    system("pause");  
    return 0;  
}  

下面我要說的是,這個算法裏面就是採用的貪心第三方案,一般這個方案是成功率最大的,其他兩個方案我在這裏沒有考慮,在這裏得到的結果是利用了115容量裝了價值195的東西,但是這明顯不是最優結果,分明還可以裝一個A進去!剛好滿足150重量,由於在算法中我單純的利用第三種貪心方法求解,當剩餘的包裹中最優的再加進來的時候已經超過了,所以這個時候可以選擇剩餘包裹中次優的(如這裏選擇A),再不行就次次優的,儘量把包裹裝滿,這樣得到的結果就很接近了(不保證一定爲最優),但是我們一般不這樣來求解,下一文章會介紹動態規劃算法來解決這個問題,動態規劃很好的彌補了貪心算法的不足!詳見下一章!!

 

還需要說明的是,如果包裹是可以拆分的,那這個問題就得到了整體最優解,前面不變,就是當最後一次裝進去已經超過容量的時候可以選擇只裝她的一部分!很多編程題一般是這種情況!

經自己總結的貪心算法幾大經典問題:

1:活動時間安排的問題

    設有N個活動時間集合,每個活動都要使用同一個資源,比如說會議場,而且同一時間內只能有一個活動使用,每個活動都有一個使用活動的開始si和結束時間fi,即他的使用區間爲(si,fi),現在要求你分配活動佔用時間表,即哪些活動佔用該會議室,哪些不佔用,使得他們不衝突,要求是儘可能多的使參加的活動最大化,即所佔時間區間最大化!

上圖爲每個活動的開始和結束時間,我們的任務就是設計程序輸出哪些活動可以佔用會議室!


void GreedyChoose(int len,int *s,int *f,bool *flag);  
  
int main(int argc, char* argv[])  
{  
    int s[11] ={1,3,0,5,3,5,6,8,8,2,12};  
    int f[11] ={4,5,6,7,8,9,10,11,12,13,14};  
  
    bool mark[11] = {0};  
  
    GreedyChoose(11,s,f,mark);  
    for(int i=0;i<11;i++)  
        if(mark[i])  
            cout<<i<<" ";  
    system("pause");  
    return 0;  
}  
  
void GreedyChoose(int len,int *s,int *f,bool *flag)  
{  
    flag[0] = true;  
    int j = 0;  
    for(int i=1;i<len;++i)  
        if(s[i] >= f[j])  
        {  
            flag[i] = true;  
            j = i;  
        }  
}  


得出結果是 0 3 7 10,也就是對應的時間段

值得說明一下,雖然貪心算法不是一定可以得到最好的解 ,但是對於這種活動時間的問題,他卻得到的總是最優解,這點可以用數學歸納法證明,在這裏,體現出來的貪心策略是:每一個活動時間的挑選總是選擇最優的,就是剛好匹配的,這樣得出的結果也就是最優的了!由於這個算法很簡單,在這裏就沒有註釋了!

類似這種題還有個區間覆蓋問題,就是說很多個區間,其中有些是相互覆蓋着的,要求去除多餘的區間,使剩下的區間佔用長度最大,實際就是這個題,只是問法變換了而已!接下來讓我們看線性覆蓋的問題,跟上面的相反!

2.貪心實例之線段覆蓋(lines cover)

題目大意:

在一維空間中告訴你N條線段的起始座標與終止座標,要求求出這些線段一共覆蓋了多大的長度。

爲了方便說明,我們採用上述表格中的數據代表10條線段的起始點和終點,注意,這裏是用起始點爲順序進行排列,和上面的不一樣,知道了這些我們就可以着手開始設計這個程序:

#include <iostream>  
using namespace std;  
  
int main(int argc, char* argv[])  
{  
    int s[10] = {2,3,4,5,6,7,8,9,10,11};  
    int f[10] = {3,5,7,6,9,8,12,10,13,15};  
    int TotalLength = (3-2);                   
  
    for(int i=1,int j=0; i<10 ; ++i)  
    {  
        if(s[i] >= f[j])  
        {  
            TotalLength += (f[i]-s[i]);  
            j = i;  
        }  
        else  
        {  
            if(f[i] <= f[j])  
                continue;  
            else  
            {  
                TotalLength += f[i] - f[j];  
                j = i;  
            }  
        }  
    }  
  
    cout<<TotalLength<<endl;  
    system("pause");  
    return 0;  
}  


運行結果爲13,顯然這是我們需要的結果,這裏註明一下,上面圖表中數據有點問題,實際以程序中給出的爲主!

 

3,:數字組合問題!

設有N個正整數,現在需要你設計一個程序,使他們連接在一起成爲最大的數字,例3個整數 12,456,342 很明顯是45634212爲最大,4個整數 342,45,7,98顯然爲98745342最大

程序要求:輸入整數N 接下來一行輸入N個數字,最後一行輸出最大的那個數字!

題目解析:拿到這題目,看起要來也簡單,看起來也難,簡單在什麼地方,簡單在好像就是尋找哪個開頭最大,然後連在一起就是了,難在如果N大了,假如幾千幾萬,好像就不是那麼回事了,要解答這個題目需要選對合適的貪心策略,並不是把數字由大排到小那麼簡單,網上的解法是將數字轉化爲字符串,比如a+b和b+a,用strcmp函數比較一下就知道誰大,也就知道了誰該排在誰前面,不過我覺得這個完全沒必要,在這裏我採用一種比較巧妙的方法來解答,不知道大家還記得冒泡排序法不,那是排序最早接觸的一種方法,我們先看看它的源代碼:

#include <iostream>  
using namespace std;  
  
int main(int argc, char* argv[])  
{  
    int array[10];  
    for(int i=0;i<10;i++)  
        cin>>array[i];  
  
    int temp;  
    for(i=0; i<=9 ; ++i)  
        for(int j=0;j<10-1-i;j++)  
            if(array[j] > array[j+1] )  
            {  
                temp = array[j];  
                array[j] = array[j+1];  
                array[j+1] = temp;  
            }  
    for(i=0;i<10;i++)  
        cout<<array[i]<<" ";  
    cout<<endl;  
    system("pause");  
    return 0;  
[cpp] view plain copy

 print?
}  


相信這種冒泡已經很熟悉了,注意看程序中最核心的比較規則是什麼,是這一句if(array[j] > array[j+1] ) 他是以數字大小作爲比較準則來返回true或者是false,那麼我們完全可以改變一下這個排序準則,比如23,123,這兩個數字,在我們這個題中它可以組成兩個數字 23123和12323,分明是前者大些,所以我們可以說23排在123前面,也就是23的優先級比123大,123的優先級比23小,所以不妨寫個函數,傳遞參數a和b,如果ab比ba大,則返回true,反之返回false,函數原型如下:  

  1. <pre class="cpp" name="code">bool compare(int Num1,int Num2)  
  2. {  
  3.     int count1,count2;  
  4.     int MidNum1 = Num1,MidNum2 = Num2;  
  5.     while( MidNum1 )  
  6.     {  
  7.         ++count1;  
  8.         MidNum1 /= 10;  
  9.     }  
  10.   
  11.     while( MidNum2 )  
  12.     {  
  13.         ++count2;  
  14.         MidNum2 /= 10;  
  15.     }  
  16.   
  17.     int a = Num1 * pow(10,count2) + Num2;  
  18.     int b = Num2 * pow(10,count1) + Num1;  
  19.   
  20.     return (a>b)? true:false;  
  21. }</pre>  
  22. <pre> </pre>  
  23. <p>好了,我們的比較準則函數也已經完成了,只需要把這個比較準則加到關鍵的地方,這個題就算完成了,最終代碼如下:</p>  
  24. <pre class="cpp" name="code">#include <iostream>  
  25. #include <cmath>  
  26. using namespace std;  
  27.   
  28. bool compare(int Num1,int Num2);  
  29. int main(int argc, char* argv[])  
  30. {  
  31.     int N;  
  32.     cout<<"please enter the number n:"<<endl;  
  33.     cin>>N;  
  34.     int *array = new int [N];  
  35.     for(int i=0;i<N;i++)  
  36.         cin>>array[i];  
  37.       
  38.     int temp;  
  39.     for(i=0; i<=N-1 ; ++i)  
  40.     {  
  41.         for(int j=0;j<N-i-1;j++)  
  42.             if( compare(array[j],array[j+1]) )  
  43.             {  
  44.                 temp = array[j];  
  45.                 array[j] = array[j+1];  
  46.                 array[j+1] = temp;  
  47.             }  
  48.     }  
  49.       
  50.     cout<<"the max number is:";  
  51.     for( i=N-1 ; i>=0 ; --i)  
  52.         cout<<array[i];  
  53.     cout<<endl;  
  54.     delete [] array;  
  55.     system("pause");  
  56.     return 0;  
  57. }  
  58.   
  59. bool compare(int Num1,int Num2)  
  60. {  
  61.     int count1=0,count2=0;  
  62.     int MidNum1 = Num1,MidNum2 = Num2;  
  63.     while( MidNum1 )  
  64.     {  
  65.         ++count1;  
  66.         MidNum1 /= 10;  
  67.     }  
  68.       
  69.     while( MidNum2 )  
  70.     {  
  71.         ++count2;  
  72.         MidNum2 /= 10;  
  73.     }  
  74.       
  75.     int a = Num1 * pow(10,count2) + Num2;  
  76.     int b = Num2 * pow(10,count1) + Num1;  
  77.       
  78.     return (a>b)? true:false;  
  1. 在貪心算法裏面最常見的莫過於找零錢的問題了,題目大意如下,對於人民幣的面值有1元 5元 10元 20元 50元 100元,下面要求設計一個程序,輸入找零的錢,輸出找錢方案中最少張數的方案,比如123元,最少是1張100的,1張20的,3張1元的,一共5張!</p>  
  2. <p>解析:這樣的題目運用的貪心策略是每次選擇最大的錢,如果最後超過了,再選擇次大的面值,然後次次大的面值,一直到最後與找的錢相等,這種情況大家再熟悉不過了,下面就直接看源代碼:</p>  
  3. <p> </p>  
  4. <pre class="cpp" name="code">#include <iostream>  
  5. #include <cmath>  
  6. using namespace std;  
  7.   
  8. int main(int argc, char* argv[])  
  9. {  
  10.     int MoneyClass[6] = {100,50,20,10,5,1};            //記錄錢的面值  
  11.     int MoneyIndex [6] ={0};                           //記錄每種面值的數量  
  12.     int MoneyAll,MoneyCount = 0,count=0;  
  13.   
  14.     cout<<"please enter the all money you want to exchange:"<<endl;  
  15.     cin>>MoneyAll;  
  16.   
  17.     for(int i=0;i<6;)                                  //只有這個循環纔是主體  
  18.     {  
  19.         if( MoneyCount+MoneyClass[i] > MoneyAll)  
  20.         {  
  21.             i++;  
  22.             continue;  
  23.         }  
  24.   
  25.         MoneyCount += MoneyClass[i];  
  26.         ++ MoneyIndex[i];  
  27.         ++ count;  
  28.   
  29.         if(MoneyCount == MoneyAll)  
  30.             break;  
  31.     }  
  32.   
  33.     for(i=0;i<6;++i)                                  //控制輸出的循環  
  34.     {  
  35.         if(MoneyIndex[i] !=0 )  
  36.         {  
  37.             switch(i)  
  38.             {  
  39.             case 0:  
  40.                 cout<<"the 100 have:"<<MoneyIndex[i]<<endl;  
  41.                 break;  
  42.             case 1:  
  43.                 cout<<"the 50 have:"<<MoneyIndex[i]<<endl;  
  44.                 break;  
  45.             case 2:  
  46.                 cout<<"the 20 have:"<<MoneyIndex[i]<<endl;  
  47.                 break;  
  48.             case 3:  
  49.                 cout<<"the 10 have:"<<MoneyIndex[i]<<endl;  
  50.                 break;  
  51.             case 4:  
  52.                 cout<<"the 5 have:"<<MoneyIndex[i]<<endl;  
  53.                 break;  
  54.             case 5:  
  55.                 cout<<"the 1 have:"<<MoneyIndex[i]<<endl;  
  56.                 break;  
  57.             }  
  58.         }  
  59.       
  60.     }  
  61.     cout<<"the total money have:"<<count<<endl;  
  62.     system("pause");  
  63.     return 0;  
  64. }</pre><pre class="cpp" name="code"> </pre><pre class="cpp" name="code"> </pre>  
  65. <p> <span style="font-size:24px; color:#ff0000">由於精力有限,貪心算法的很多題還沒寫,以後有時間會補上,其實最主要就是記住貪心策略,每次選擇的都是對於當前而言最優的,貪心思想不難,利用好就需要多練習,望一起進步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章