算法設計與分析基礎(第三版)之第8章 動態規劃

本博客主要是對Anany Levitin著、潘彥譯的算法設計與分析基礎(第三版)之第8章 動態規劃進行代碼梳理與講解,代碼語言主要爲Python,其他語言也類似,可以把Python的解題方法看成是僞代碼進行轉換。
動態規劃是數據結構中最重要的一環,也是面試中大家覺得頭大的一類面試題型。關於動態規劃的解題方法可以參考博主另一篇博文:一文詳解動態規劃【更新ing】。這篇博文的主要目的在於對書中的題目進行代碼化。
例題1:幣值最大化問題
這個是個經典題目了,也有多種變體,我們先看書中題目。題目是這個樣子的:
給定一排n{n}個硬幣,其面值均爲正整數c1,c2,...,cn{c_1, c_2, ..., c_n},這些整數並不一定兩兩不同。請問如何選擇硬幣,使得其原始位置互不相鄰的條件下,所選硬幣總額最大。
該題是一個動態規劃的典型題目,重點在於理解原始位置互不相鄰及子問題的構建上。整體上,解決動態規劃問題四步走:
(1)劃分子問題
(2)構建狀態轉移
(3)確定初始狀態
(4)循環迭代求解
我們思考如下:
1、題目理解:
       互不相鄰,我們可以理解爲下標間隔2,即1,3,5,...,2n+1{1, 3, 5,..., 2n+1 } 或者0,2,4,...,2n{0, 2, 4,..., 2n}這種方式,原始位置一定互不相鄰。
2、劃分子問題
       我們自頂向下思考這個問題,如果說我們對於已經選擇好的硬幣組合f(n)f(n),代表的含義爲我們已經選擇好下標到nn的硬幣組合,此時的總額記爲f(n)f(n),那麼對於f(n)f(n)來說,它的選擇計劃對於最後一個硬幣cnc_n有兩個來源:
(1)選擇硬幣cn{c_n},因題目要求選擇的是互不相鄰的位置,那麼子問題就變成必須要求f(n2)f(n-2)最大,此時f(n)=f(n2)+cn{f(n) = f(n-2) + c_n}
(2)不選擇硬幣cn{c_n},此時子問題變成—>如何在c1,c2,...,cn1{c_1, c_2, ..., c_{n-1}}中選擇硬幣,使得其要求總額最大。即要求f(n1)f(n-1)最大就好。
3、構建狀態轉移
       基於(1)、(2),對於f(n)f(n)整體上來說,總額最大的關係表達式爲:
f(n)=max(f(n2)+cn,f(n1)){f(n) = max( f(n-2) + c_n, f(n-1))}
完成了動態規劃解題第一步劃分子問題和第二步構建狀態轉移。
3、確定初始狀態
       這裏初始狀態有兩個:n=0n=1n=0和n=1這兩個狀態。
       當n=0n=0時,表明此時硬幣數組爲空,不論如何選擇,最大金額一定爲0,即f(0)=0f(0)=0
       當n=1n=1時,表明此時硬幣數組只有一個元素,而且該元素爲正整數。不論如何選擇,最大金額一定爲該元素,即f(1)=c1f(1)=c_1
4、循環迭代求解
先設定一個長度爲nn【爲什麼是nn而不是n+1n+1呢?歡迎評論區討論~】的數組arrarr用來裝一排i,0<=i<=ni, 0<=i<=n的硬幣中選擇互不相鄰的硬幣的最大總金額,那麼從一排n個硬幣中選擇出互不相鄰的硬幣總金額arr[n]arr[n]

class CoinSelection(object):
    def __init__(self, n, arr_coin):
        self.n = n
        self.arr_coin = arr_coin
    def solution(self):
        if self.n == 0 or self.arr_coin is None:
            return 0
        else:
            arr_solution = [0] * self.n
            arr_solution[0] = 0
            arr_solution[1] = self.arr_coin[0]
            for coin_number in range(2, n, 1):
                arr_solution[coin_number] = max(arr_solution[coin_number-2] + self.arr_coin[coin_number], arr_solution[coin_number - 1] )
            return arr_solution[-1]


if __name__ == "__main__":
    n = 6
    coin_arr = [5, 1, 2, 10, 6, 2]
    coin_solution = CoinSelection(n, coin_arr)
    result = coin_solution.solution()
    print(result)

此種接法的空間複雜度和時間複雜度均爲O(n)O(n)

例題2:找零問題
需找零金額爲nn,最少要用多少面值爲d1<d2<...<dmd_1<d_2<...<d_m的硬幣?假設mm種面值爲d1<d2<...<dmd_1<d_2<...<d_m的硬幣數量無限可得,d1=1d_1=1
該題思考:
一開始這個題目博主理解有誤,主要在對【找零金額】這四個字的理解上,博主開始理解成了找零錢,也就是數學中的餘數,然後就納悶:這個題目也沒告訴博主需要付多少錢啊,零錢怎麼找呢?懵逼了一圈後,看了解答發現,這裏的找零金額是指總金額。後續解答講解中博主都用總金額替換掉找零金額。。
假設f(n)f(n)表示選用最少的硬幣組成的總金額爲nn,上一個階段爲從mm個面值硬幣中任意選一個,且組成硬幣總額爲ndjn-d_j的所需硬幣數最少,即f(n)=minf(ndj)+1f(n) = min{f(n-d_j)} + 1
初始邊界條件:f(0)=0f(0) = 0f(1)f(1)表示總金額爲1,此時只能選擇d1d_1,故而只有選擇d1d_1這一種方式,因此f(1)=1f(1) = 1

import numpy as np


class SumMoney(object):
    def __init__(self, n, coin_value):
        self.n = n
        self.coin_value = coin_value

    def solution(self):
        coin_solution = [0] * (self.n + 1)
        coin_solution[0] = 0
        coin_solution[1] = 1
        for sum_money in range(2, self.n + 1, 1):
            tmp = np.inf
            for single_coin_value_index in range(0, len(self.coin_value)):
                if sum_money >= self.coin_value[single_coin_value_index]:
                    tmp = min(coin_solution[sum_money - self.coin_value[single_coin_value_index]], tmp)
            coin_solution[sum_money] = tmp + 1
        return coin_solution[self.n]


if __name__ == "__main__":
    n = 6
    coin_arr = [1, 3, 4]
    coin_solution = SumMoney(n, coin_arr)
    result = coin_solution.solution()
    print(result)

此時的輸出結果爲:

2

Process finished with exit code 0

變體1:
輸出例題2的具體最優解即最少的硬幣組合方案中使用了哪些硬幣。
尋找組合的過程與例題2的思路反着來,即先找到最少的硬幣組合數,然後從最少硬幣組合數的最後一個數字出發,看其所選硬幣數,然後再往上反着推。
此題需要對例題2的每一步的最少硬幣數用到的硬幣進行存儲,然後在找到最少硬幣後反推着來。

變體2:對於每種面值的硬幣限制其使用數量

例題3:最短路徑問題

Ref:
1、硬幣找零問題–動態規劃參考
2、動態規劃從入門到專家

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