一个简单的例子由易到难理解动态规划
在本文中,我們先用最簡單的方法來解決問題,然后一步步進階,在這個不斷優化算法的過程中理解動態規劃。
力扣 322. 零錢兌換 問題
1. 暴力遞歸
假設 coins[] = [2, 4, 5],amount = 10,想要求 amount = 10 可以求
min ( 1 + coinChange(coins, amount - 2), 1 + coinChange(coins, amount - 4), 1 + coinChange(coins, amount - 5) )
即假設已經知道了 amount = 8、amount = 6、amount = 5 時 所需的最少的硬幣個數, 那只要再加 1 個硬幣就可以湊夠 10,在這三者中選最少的一個就是答案。
繼續想的話,求 amount = 8,也是同樣的方法,就是不斷往下遞歸。
public int coinChange(int[] coins, int amount) {//確定遞歸結束的條件if(amount == 0) return 0;if(amount < 0) return -1;int res = Integer.MAX_VALUE;//遍歷求各 coinChange(amount-coin) 的值,求出最小值for(int coin: coins){int subProblem = coinChange(coins, amount-coin);if(subProblem == -1) continue;res = Math.min(res, subProblem + 1);}return res==Integer.MAX_VALUE ? -1 : res; }為了更好理解,來畫一下遞歸樹
從上圖就可以看出來,這種方法就是在用遞歸把所有的可能窮舉出來,然后取最小值。
但這個方法存在重疊子問題,即存在大量的重復計算。有很多問題我們已經計算過,但當遞歸到那里時,還是要再算一次。
所以為解決這個問題,下面采用帶備忘錄的遞歸解法。
2. 帶備忘錄的遞歸解法
帶備忘錄就是把已經計算過的值存起來,以此來減少不必要的計算。
每次計算完后,將值存入一個數組中,每次計算前判斷一下備忘錄中有沒有存,有則可以直接獲取,不需要再進行漫長的遞歸運算了。
int[] count; public int coinChange(int[] coins, int amount) {//建立數組,進行初始化count = new int[amount+1];Arrays.fill(count, -2);return(coin(coins, amount)); } public int coin(int[] coins, int amount){if(amount == 0) return 0;if(amount < 0) return -1;//判斷是否存入備忘錄if(count[amount] != -2)return count[amount];int res = Integer.MAX_VALUE;for(int coin:coins){int subProblem = coin(coins, amount-coin);if(subProblem == -1) continue;res = Math.min(res, subProblem+1);}//存入count[amount] = (res == Integer.MAX_VALUE) ? -1 : res;return count[amount]; }現在似乎已經很完善了,但備忘錄解法是 自頂向下 的,是從一個規模較大的原問題,一層一層向下分解成子問題,直至底層,然后再逐層返回結果。
動態規劃是 自底向上 的解出最優值。下面來改造一下這個方法。
3. dp 數組的迭代解法
采用 自底向上 就是要從底層狀態一直算到所求狀態
定義一個 dp 數組,dp[i] 表示為 amount = i 時所需的最少的硬幣個數
這就是最后的動態規劃的解法了。
動態規劃最核心的是寫出狀態轉移方程,這也是最困難的一步,多多練習,熟能生巧。
本文源于學習 labuladong的算法小抄 ,大佬寫的明白易懂,沒有什么晦澀的地方。
總結
以上是生活随笔為你收集整理的一个简单的例子由易到难理解动态规划的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ACdream 1069 无耻的出题人
- 下一篇: BZOJ 1069 最大土地面积(旋转卡