貪心算法和動態規劃的思路及其Python實現

貪心算法

百度的定義: 貪心算法(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的是在某種意義上的局部最優解。

通俗一點講,當要解決某一個問題時,先判斷第一步的最優解,然後把剩下的步驟看作下一個遞歸的具體問題。

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

假設具體問題數值:A物品,重量爲6kg,價值爲8元,

                             B物品,重量爲8kg,價值爲13元,

                             C物品,重量爲10kg,價值爲15元

揹包可以裝爲50kg的物品。

有經驗的小朋友肯定首先判斷拿取哪一個物品既輕又有價值。A物品:單位重量的價值爲8/6(元)

                                                                                                 B物品:單位重量的價值爲13/8(元)

                                                                                                 C物品:單位重量的價值爲15/10(元)

計算看出,只要能裝,首先要拿B物品,因爲單位價值最高。

即剩餘50kg

第一步:我有 50 kg揹包,我可以選擇的物品有 A,B,C

第二步:選擇當前這一步最優解,即 B。而問題的揹包變成50-8=42kg,而問題只有揹包重量改變,重新回到第一步判斷。

所以就是:(選擇優先B,然後C,然後A)

1:我有  50kg  揹包,我可以選擇A,B,C,總價值爲0    ,我選擇B

2:我有  42kg  揹包,我可以選擇A,B,C,總價值爲13  ,我選擇B

3:我有  34kg  揹包,我可以選擇A,B,C,總價值爲26  ,我選擇B

4:我有  26kg  揹包,我可以選擇A,B,C,總價值爲39  ,我選擇B

5:我有  18kg  揹包,我可以選擇A,B,C,總價值爲52  ,我選擇B

6:我有  10kg  揹包,我可以選擇A,B,C,總價值爲65  ,我選擇B

7:我有  2kg    揹包,我可以選擇               ,總價值爲78  ,沒有空間選了


這時候有眼尖的小朋友就問了:“那這道題這樣做的話,只能取6個B物品,重量爲48kg,總價值爲6*13=78元。那如果我取5個B物品,1個C物品,不是剛好50kg嗎?這樣總價值有5*13+15=80元呢”。其實這就看出,貪心算法得到的並不是最優解

如何用代碼實現呢?

# coding=utf-8
if __name__ == '__main__':
    beg = 50                       #揹包50kg
    value = 0                      #已經獲得的價值
    choice = []
    while beg > 0:                 #如果揹包還有空位,則遞歸
        if beg >= 8:               #選擇當前這一步的最優解,既選擇B商品
            beg = beg - 8
            value = value + 13
            choice.append("B")
        elif beg >= 10:            #要是B商品選擇不了,則選擇第二單位價值的物品,即A物品
            beg = beg - 10
            value = value + 15
            choice.append("A")
        elif beg >= 6:
            beg = beg - 6
            value = value + 8
            choice.append("C")
        else:                      #當所有的物品都選擇不了,則退出
            break
    print "剩餘的揹包重量:",beg
    print "獲得的總價值:",value
    print "選擇的物品的類型及順序:",choice

貪心算法只能保證次優解


動態規劃

動態規劃可以看一下http://www.cnblogs.com/sdjl/articles/1274312.html提及到的金礦例題,我覺得算是我見過講得最簡練的博客了(膜拜..)
裏面主要提到動態規劃的6個思考點:(以下是借鑑上述博客作者的思路,再加以延伸)

1:最優子結構

好像貪心一樣,動態規劃也是把一個複雜問題分成一個一個步驟,而與貪心不一樣的是,貪心是保證當前這一步是當前問題的最優解,而不保證這一步屬於整個複雜問題的最優解。而最優子結構就是要規定當前這一步是整個複雜問題的最優解。(區別可參考一開始那個揹包問題,最後一步究竟取B,還是取A。)

2:子問題重疊

子問題重疊在貪心也需要體現,走每一步面對的問題都是同一個類型的,例如揹包問題的子問題就是:當前我的揹包有Xkg空間,我能選擇XXX。例如在金礦問題的子問題就是:當前我有X人,XXX剩餘金礦。

3:邊界

邊界體現在揹包沒有空間,或者金礦沒有足夠人數取挖。

4:子問題獨立

子問題即上面走的每一步之間沒有干擾(互不影響)

----------------------------------------------------------------------------------------------------

看完這裏還是迷迷糊糊?不急,還沒說完,上面那四個點只是基礎,還需要剔除重複的步驟。(相當於深搜的剪枝吧..)

這裏我再引入一道題:斐波那契數列之青蛙跳臺階。

一隻青蛙一次可以跳上 1 級臺階,也可以跳上2 級。求該青蛙跳上一個n 級的臺階總共有多少種跳法。

這道題其實用動態規劃可以解決。一般的想法:

# coding=utf-8
def fib(n):          #當前有N個臺階,可以選擇跳一個臺階,也可以選擇跳兩個
    if n <=1 :       #邊界問題,要是當前只剩下一個臺階,則只剩下一個方法跳。
        return 1
    else:            #跳一個臺階和跳兩個臺階都是一個選項。
        return fib(n-1)+fib(n-2)
print fib(5)
這種最簡單的方法是可以實現少臺階的情況,一旦多情況(100個臺階)就運行不下去。http://blog.csdn.net/baidu_28312631/article/details/47418773博客作者闡述瞭如果動態規劃不優化的話,答案可能性有多少。在這裏我用圖列出一共有多少種可能性。

每到葉結點的0代表剩下0步,確定唯一的跳臺階的唯一確定方案。上面一共有8個葉結點,即有8種方案。不過這裏我們不是討論這道題的解,而是討論動態規劃的優化。

當我剩下1層臺階時,我只有兩種可能,就是這裏(或者從2到0)

咦,那當我只剩下2層臺階時,我也只有一種可能性,就是

當我剩下3層臺階時,也只有

剩下4層臺階時,有



這樣的話,其實我有5層臺階時,其中包括1個(4層臺階子樹),2個(3層臺階子樹),3個(2層臺階子樹),5個(從1到0),4個(從2直接到0)。

這樣我用一個“備忘錄”(動態規劃的一個專用名詞)記錄我計算過的樹結構的話,5層臺階的備忘錄就只有5項,而不優化的話,我就要計算答案那麼多的可能性(8種),一旦臺階數變多,答案的可能性個數會飛速上升。
經過優化的跳臺階的動態規劃:(網上有個版本是用裝飾器在未優化的基礎上添加功能,我這裏簡單用普通方法寫一遍..)
代碼如下:
class Dp1(object):                                     #動態規劃類
    def __init__(self,n):                              #初始化
        self.mark = [0 for _ in xrange(n+1)]           #定義一個一維數組,初始化全部爲0,長度爲臺階數。用來當作“備忘錄”。
        print self.dp(n)                               #開始遞歸
    def dp(self,n):                                    #遞歸的方法
        self.m = 0                                     #m的含義是當前n個臺階有m種跳法
        if self.mark[n] != 0:                          #先從備忘錄尋找n,若存在mark[n]不等於0,則代表曾經計算過,n個臺階有mark[n]種跳法
            self.m = self.mark[n]                      #若備忘錄有,則直接得到n層臺階的答案
        elif n <= 0:                                   #從這裏開始的四行是用來判斷“邊界問題”
            if n == 0:                                 #若剛好跳完臺階,則這樣算一種方法
                self.m = 1                             #m變成1,代表是一種可行方法
            else:                                      #有可能跳的臺階超過實際臺階數
                self.m = 0                             #m爲0,代表不可行
        elif n>0:                                      #這裏兩行是用於規劃轉移方程式(其實這裏很簡單),青蛙只有兩種可能,跳一層或者跳兩層。
            self.m = self.dp(n-2)+self.dp(n-1)         #當前n層臺階的解個數 等於 n-1層臺階的解 + n-2層臺階的解
        self.mark[n] = self.m                          #把m放入備忘錄,下次若是再次是n層臺階,則不用計算直接取備忘錄的數。(優化)
        return self.m                                  #返回

if __name__ == '__main__':
    dp1 = Dp1(100)


所以其實一開始沒有寫錯,是6個思考點,是經過優化的6個思考點,第五步是做備忘錄,第六步是時間分析。
總結步驟的話,根據博客作者的話來說,就是:

       1、構造問題所對應的過程。

       2、思考過程的最後一個步驟,看看有哪些選擇情況。

       3、找到最後一步的子問題,確保符合“子問題重疊”,把子問題中不相同的地方設置爲參數。

       4、使得子問題符合“最優子結構”。

       5、找到邊界,考慮邊界的各種處理方式。

       6、確保滿足“子問題獨立”,一般而言,如果我們是在多個子問題中選擇一個作爲實施方案,而不會同時實施多個方案,那麼子問題就是獨立的。

       7、考慮如何做備忘錄。

       8、分析所需時間是否滿足要求。

       9、寫出轉移方程式。


一開始的金礦例題是用C++實現的,我這裏換成python。
這是根據開頭提到的博客作者代碼思路改編的。
class Dp(object):
    def __init__(self, n, m, peopleneed, gold):        #n是總人數,m是金礦數
        self.peopleneed = peopleneed                   #每一個金礦開挖需要的人數
        self.gold = gold                               #每一個金礦的金礦數
        self.maxgold =[[-1 for i in xrange(n)] for i in xrange(m)]   #初始化備忘錄,創建一個m行n列的二維數組。
        print self.getmaxgold(n,m)                 #n,m減一是因爲數組是從0開始

    def getmaxgold(self,n,m):
        #retmaxgold = 0
        if self.maxgold[m-1][n-1] != -1:
            retmaxgold = self.maxgold[m-1][n-1]
        elif m == 0:
            if (n >= self.peopleneed[m-1]):
                retmaxgold = self.gold[m-1]
            else:
                retmaxgold = 0
        elif n >= self.peopleneed[m-1]:
            retmaxgold = max(self.getmaxgold(n - self.peopleneed[m-1],m -1)+self.gold[m-1],self.getmaxgold(n,m-1))
        else:
            retmaxgold = self.getmaxgold(n,m-1)
        self.maxgold[m-1][n-1] = retmaxgold
        return retmaxgold

if __name__ == '__main__':
    peopleneed = []
    gold = []
    str = raw_input("")
    try:
        str = str.split(" ")
        n = int(str[0])
        m = int(str[1])
        for i in range(m):
            goldmount = raw_input("")
            goldmount = goldmount.split(" ")
            peopleneed.append(int(goldmount[0]))
            gold.append(int(goldmount[1]))
    except:
        print "輸入格式錯誤"
    dp = Dp(n, m, peopleneed, gold)



輸入格式爲:

100 5

77 92

22 22

29 87

50 46

99 90

答案: 133




下面找了一些題目練習(動態規劃只有練習才能提高...只能不斷練習)

例題來源:http://www.cnblogs.com/wuyuegb2312/p/3281264.html#q1

1.硬幣找零

難度評級:★

  假設有幾種硬幣,如1、3、5,並且數量無限。請找出能夠組成某個數目的找零所使用最少的硬幣數。

代碼實現:

# coding=utf-8
class Dp2(object):
    def __init__(self,money):
        self.mark = [0 for _ in xrange(money+1)]                   #備忘錄
        print self.dp(money)                                       #開始遞歸
    def dp(self,money):  
        self.coin = 0                                              #需要的硬幣數爲0
        if self.mark[money] != 0:                                  #在備忘錄中尋找該金額下的最少硬幣找零數,若存在,則取出
            self.coin = self.mark[money]
        elif money <= 0:                                           #邊界問題
            if money == 0:                                         #如果金額爲零,則代表剛好算是一種找零方法
                self.coin = 0                                      #這裏的0不是代表硬幣數爲0,而是代表這種方法可行,因爲在下面已經有加1,若是這裏coin爲1,結果就會比答案多1
            else:
                self.coin = float("inf")                           #若是金額爲負數,即“拿多了”,這種方法不可行,則硬幣消耗數爲 無窮大
        elif money > 0:
            self.coin = min(self.dp(money-1),self.dp(money-3),self.dp(money-5))+1     #遞歸,找出最少的可以湊齊金額數money的方法
        self.mark[money] = self.coin                               #做備忘錄
        return self.coin

if __name__ == '__main__':
    dp2 = Dp2(65)               #找零錢

之後慢慢再更新題目



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