2.動態規劃
動態規劃
通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。
動態規劃常常適用於有重疊子問題和最優子結構性質的問題。
基本思想
若要解一個給定問題,我們需要解其不同部分(即子問題),再合併子問題的解以得出原問題的解。 通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化儲存,以便下次需要同一個子問題解之時直接查表。 這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。
最優子結構:當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。
重疊子問題:在用遞迴演算法自頂向下解問題時,每次產生的子問題並不總是新問題,有些子問題被反覆計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而後將其解儲存在一個表格中,在以後儘可能多地利用這些子問題的解。
分治與動態規劃
共同點:二者都要求原問題具有最優子結構性質,都是將原問題分而治之,分解成若干個規模較小(小到很容易解決的程式)的子問題。然後將子問題的解合併,形成原問題的解.
不同點:分治法將分解後的子問題看成相互獨立的,通過用遞迴來做。
動態規劃將分解後的子問題理解為相互間有聯絡,有重疊部分,需要記憶,通常用迭代來做。
例項
最少找零問題
最少硬幣找零問題為:給予不同面值的硬幣若干種種(每種硬幣個數無限多),用若干種硬幣組合為某種面額的錢,使硬幣的的個數最少。(演算法詳細介紹)
在現實生活中,我們往往使用的是貪心演算法,比如找零時需要13元,我們先找10元,再找2元,再找1元。這是因為現實生活中的硬幣(紙幣)種類特殊。如果我們的零錢可用的有1、2、5、9、10。我們找零18元時,貪心演算法的策略是:10+5+2+1,四種,但是明明可以用兩個9元的,這就設計動態規劃。
class DynamicProgramming: def __init__(self): pass def leet_code(self, coin_list, change, min_coins, coins_used): for cents in range(change + 1): # 依次迴圈從0到所需兌換面值的每一個面值 coin_count = cents # 初始化最優解為當前面值數,即兌換硬幣個數,最大為每個都是1元硬幣 new_coin = 1 # 初始化找零硬幣面值列表中的面值 for j in [c for c in coin_list if c <= cents]: # 在不大於要找零的硬幣面值列表中迴圈 if min_coins[cents - j] + 1 < coin_count: # 尋找最小的min_coins[cents - j]+1,其中j屬於coin_list coin_count = min_coins[cents - j] + 1 # 臨時儲存當前面值的最優解,當前最優硬幣個數 new_coin = j # 將當前硬幣面值j臨時儲存為當前找零面值在找零硬幣面值列表中的對應值 min_coins[cents] = coin_count # 記錄當前找零面值在找零最優解列表中的最優解 coins_used[cents] = new_coin # 記錄當前找零面值在找零硬幣面值列表中對應的值 return min_coins[change] # 返回待找零數值的最優解 # 獲取最終找零的硬幣面值 def print_coins(self, coins_used, change): coins = [] while change > 0: this_coin = coins_used[change] # 從找零硬幣面值列表中獲取對應的硬幣面值 coins.append(this_coin) change = change - this_coin # 去除該面值後繼續迴圈獲取 return coins dp = DynamicProgramming() change = 12 coin_list = [1, 5, 7, 10] coins_used = [0] * (change + 1) coin_count = [0] * (change + 1) res = dp.leet_code(coin_list, change, coin_count, coins_used) coins = dp.print_coins(coins_used, change) print("最少硬幣:", res) print("硬幣兌換列表:", coins)
最長公共子序列
0-1揹包問題
最優二叉樹