本博客主要是對Anany Levitin著、潘彥譯的算法設計與分析基礎(第三版)之第8章 動態規劃進行代碼梳理與講解,代碼語言主要爲Python,其他語言也類似,可以把Python的解題方法看成是僞代碼進行轉換。
動態規劃是數據結構中最重要的一環,也是面試中大家覺得頭大的一類面試題型。關於動態規劃的解題方法可以參考博主另一篇博文:一文詳解動態規劃【更新ing】。這篇博文的主要目的在於對書中的題目進行代碼化。
例題1:幣值最大化問題
這個是個經典題目了,也有多種變體,我們先看書中題目。題目是這個樣子的:
給定一排個硬幣,其面值均爲正整數,這些整數並不一定兩兩不同。請問如何選擇硬幣,使得其原始位置互不相鄰的條件下,所選硬幣總額最大。
該題是一個動態規劃的典型題目,重點在於理解原始位置互不相鄰及子問題的構建上。整體上,解決動態規劃問題四步走:
(1)劃分子問題
(2)構建狀態轉移
(3)確定初始狀態
(4)循環迭代求解
我們思考如下:
1、題目理解:
互不相鄰,我們可以理解爲下標間隔2,即 或者這種方式,原始位置一定互不相鄰。
2、劃分子問題
我們自頂向下思考這個問題,如果說我們對於已經選擇好的硬幣組合,代表的含義爲我們已經選擇好下標到的硬幣組合,此時的總額記爲,那麼對於來說,它的選擇計劃對於最後一個硬幣有兩個來源:
(1)選擇硬幣,因題目要求選擇的是互不相鄰的位置,那麼子問題就變成必須要求最大,此時
(2)不選擇硬幣,此時子問題變成—>如何在中選擇硬幣,使得其要求總額最大。即要求最大就好。
3、構建狀態轉移
基於(1)、(2),對於整體上來說,總額最大的關係表達式爲:
完成了動態規劃解題第一步劃分子問題和第二步構建狀態轉移。
3、確定初始狀態
這裏初始狀態有兩個:這兩個狀態。
當時,表明此時硬幣數組爲空,不論如何選擇,最大金額一定爲0,即
當時,表明此時硬幣數組只有一個元素,而且該元素爲正整數。不論如何選擇,最大金額一定爲該元素,即。
4、循環迭代求解
先設定一個長度爲【爲什麼是而不是呢?歡迎評論區討論~】的數組用來裝一排的硬幣中選擇互不相鄰的硬幣的最大總金額,那麼從一排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)
此種接法的空間複雜度和時間複雜度均爲。
例題2:找零問題
需找零金額爲,最少要用多少面值爲的硬幣?假設種面值爲的硬幣數量無限可得,。
該題思考:
一開始這個題目博主理解有誤,主要在對【找零金額】這四個字的理解上,博主開始理解成了找零錢,也就是數學中的餘數,然後就納悶:這個題目也沒告訴博主需要付多少錢啊,零錢怎麼找呢?懵逼了一圈後,看了解答發現,這裏的找零金額是指總金額。後續解答講解中博主都用總金額替換掉找零金額。。
假設表示選用最少的硬幣組成的總金額爲,上一個階段爲從個面值硬幣中任意選一個,且組成硬幣總額爲的所需硬幣數最少,即。
初始邊界條件:,表示總金額爲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、動態規劃從入門到專家