动态规划思路和Python解决零钱兑换问题和最大乘积子序列的乘积的问题
動態規劃(Dynamic Programming)思路和Python解題示例
動態規劃是一種主要用來優化樸素遞歸的方法,每當輸入不同值調用遞歸函數出現大量重復的(子)輸入和調用(返回結果)時,就可以考慮使用動態遞歸的方式來優化復雜度。
動態規劃的主要思想是存儲子問題的結果,以便于在接下來可能出現的的重復子問題中直接使用已有的結果,這樣子便可以將時間復雜度從指數級別降低到多項式(nlogn…)或線性級別,是一種以空間換時間的算法。
需要提及一點的是,動態規劃(Dynamic Programming)中的programming并不是編程的意思,規劃也不是指要望向很長遠的未來去計劃,programming可以理解為遞推,也就是問題的解決方法存在F(n)=F(n-1)+F(n-2)這樣類似的關系,每一步的結果都與他前面/相鄰的一步或幾步的結果有關,由這相鄰的一步或幾步推出而得到當前步的結果
動態規劃解法的關鍵點在于:
以一個斐波那契數列的為示例:
樸素遞歸寫法:
復雜度為O(2^n)
添加了記憶化緩存后的斐波那契解法:
復雜度為O(n)
這種解法通過儲存子結構(例如計算fib(5)時fib(3)就是fib(5)的子結構)的最優解(這里只有唯一解也是最優解),使得在計算較大的問題(例如fib(7))時,能夠利用之前較小問題(fib(3))計算過的步驟,從而避免了重復計算帶來的復雜度
單獨執行一次fib_with_cache(3)需要執行3次該函數
先執行一次n=2再執行一次n=3,fib_with_cache(3)只需要執行3次該函數
>>> print(fib_with_cache(2)) # 3 >>> print(fib_with_cache(3)) # 3 執行次數+1 執行次數+1 執行次數+1 1 執行次數+1 執行次數+1 執行次數+1 2
這里的遞推方程的方式可以寫成f(n) = f(n-1) + f(n-2)
實例和解題思路
不同路徑問題
給定一個m×n的格子,最左上格子為起點,最右下格子為終點,只能向右或者向下走,求解從起點到終點的總路徑數
以1個3×3的格子為例
給各個格子命名
初始站在起點A,那么路徑肯定就只有1條,那么從A→B的路徑有1條,從A→D的路徑有1條
從A→E的路徑有2條:A→B→E和A→D→E
A→E路徑數就等于A→B路徑數與A→D路徑數之和,也就是1+1=2
實際上可以推斷出,從起點A到任何一個格子的路徑數都等于這個格子的左邊的格子的路徑數+上方的路徑數
因此可以總結出這個問題的遞推方程:
dp[h][v] = dp[h - 1][v] + dp[h][v - 1]其中dp是一個m×n的二維數組,h和v分別為水平和垂直方向的索引
沿著上面的分析思路,最后結果就存放在數組的最右下角dp[m-1][n-1]
輸出
6 28不同路徑2
在一個m×n的格子中,最左上的一個格子為起點,最右下的一個格子為終點,其中值為X的格子代表障礙,不能經過,從起點出發只能往右或者往下前進,求從起點到終點的路徑總數
相比于上一個問題,這里定義dp方程時需要考慮兩種情況
if dp[i][j] == "空地":opt[i][j] = opt[i-1][j] + opt[i][j-1] else: // 障礙opt[i][j] = 0其中opt[i][j]表示在(i, j)位置時的(最少要走的)路徑數,也就是最優解/最優子結構
這里以反向遞推的方式,從終點到起點來遞推路徑的條數(實際上使用遞歸的解法時從終點往起點回推的方式更符合我們的邏輯)
解法和上一題基本一樣,就是多了一個判斷障礙的情況,還有這里用的是反向遞推的思路,會更容易理解
輸出
再列舉出DP、回溯和貪心算法的特性和區別,加深對DP的理解:
回溯(遞歸):窮舉式的重復計算
貪心:每次都選擇當前遇到部分的問題中的局部最優解
DP:找出并記錄(部分)子結構最優解,然后用動態轉移方程推導出下一個狀態(位置)的最優解
回溯+記錄局部最優解避免重復計算就是DP,也就是說DP是回溯+貪心的組合
習題部分
一、零錢兌換問題(leetcode #322)
給定不同面額的硬幣coins,例如[1, 2, 5],和一個總金額amount,例如3,計算滿足總金額amount所需的最少的硬幣數(給的例子要返回2,因為最少為1+2,2個硬幣),如果無法滿足就返回-1,
注:硬幣的數量沒有限制,硬幣面額最小為1
經過分析,解決這個問題需要注意以下幾點:
- 優先選擇最大的面額不一定是最優解,如coins = [1, 3, 8, 9],amount = 11,11 = 8+3→2個和11 = 9+1+1→3個,所以優先選擇最大面額不一定是最優解(貪心解法,不一定會得到最終最優解)
解題的關鍵,找出狀態關系和列出狀態轉移方程
組成amount的最少硬幣數(最終最優解)等于amount減去面額列表conis中某個面額得到的前一個總金額(狀態)amount - coins[x]的最優解+1,這個1就是減去的那個面額對應的1個硬幣,x可能是conins中的任意一個面額
也就得到了如下的遞歸思路
然后繼續思考,假設面額amount為x,例如amount = 11,那么不論何種情況下它最多需要x個硬幣(那么這個例子里為11),因為硬幣面額最小為1,從反向遞推的思路出發,從面額最小,即amount = min(coins)遞推到總金額amount,求解所需的最小硬幣數
from typing import Listclass Solution:def coinChange(self, coins: List[int], amount: int) -> int:ceiling = amount + 1mem_arr = [0] + [ceiling] * amountfor coin in coins:for i in range(coin, ceiling):mem_arr[i] = min(mem_arr[i], mem_arr[i-coin] + 1) # key:return mem_arr[amount] if mem_arr[amount] < ceiling else -1S = Solution() print(S.coinChange([0], 0)) print(S.coinChange([0], 1)) print(S.coinChange([1, 2], 1)) print(S.coinChange([1, 2], 2)) print(S.coinChange([1, 2], 3)) print(S.coinChange([3, 2, 1, 0, 5], 6))輸出
0 -1 1 1 2 2二、求最大乘積子序列(的乘積)
題目要求:
在一個整數序列中尋找最大乘積的子序列的乘積,例如
arr = [5, 0, -4, 2, -3] result = 24因為-4×2×(-3) = 24
arr = [2, -1, 1, 1] result = 2
arr = [3] result = 3
根據前面遞推的思路
遍歷nums所有元素,到當前元素nums[j]為止的最大連乘積可能等于
總之,到當前元素nums[j]為止的最大連乘積為1,2,3的情況的最大值
遍歷時需要找到到達當前步數為止的最大(正數)乘積和最小(負數)乘積
設以nums[i]結尾的最大連續子串的乘積為max_curr,以nums[i]結尾的最小連續子串的乘積為min_curr
那么到當前步數j時的
最大連續子串乘積max_curr = max(max_curr*nums[j], nums[j], min_curr*nums[j])
最小連續子串乘積min_curr = min(max_curr*nums[j], nums[j], min_curr*nums[j])
也就是我們的狀態轉移方程
初始狀態:
max_curr = min_curr = nums[0], max_li = [nums[0]]
將每一步的max_curr放入到max_li中,最終的結果為max(max_li)
輸出
24總結
以上是生活随笔為你收集整理的动态规划思路和Python解决零钱兑换问题和最大乘积子序列的乘积的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Pytorch神经网络实战案例】07
- 下一篇: ERROR 2384 — [ main]