动态规划与贪心算法
動態(tài)規(guī)劃與貪心算法
- 關于
- 什么是動態(tài)規(guī)劃
- 如何使用動態(tài)規(guī)劃
- 遞歸寫法(記憶型遞歸)
- 遞推寫法
- 動態(tài)規(guī)劃與分治區(qū)別
- 什么是貪心算法
- 如何使用貪心算法
- 動態(tài)規(guī)劃與貪心算法區(qū)別
關于
最近常看到動態(tài)規(guī)劃與貪心算法,有一點點小小的心得,寫在這里
動態(tài)規(guī)劃是一種非常精妙的算法,它沒有固定的寫法,常需要具體問題具體分析
貪心算法是一種特殊的動態(tài)規(guī)劃,具體怎么特殊,從下面可以看出
什么是動態(tài)規(guī)劃
動態(tài)規(guī)劃(Dynamic Programming, DP) 是一種用來解決一類最優(yōu)化問題的算法思想
動態(tài)規(guī)劃將一個復雜的問題分解為若干個子問題,綜合子問題的最優(yōu)解來獲得原問題的最優(yōu)解
需要注意的是,動態(tài)規(guī)劃會將每個子問題的解記錄下來,這樣遇到相同的子問題時可以直接使用結果,從而避免重復運算
如何使用動態(tài)規(guī)劃
遞歸寫法(記憶型遞歸)
使用一個額外的數組(dp數組)來記錄算過的值,例如下面的rec數組
/*** 帶備忘錄的fib* * @param n* @return*/public static int fibRecord(int n) {int[] rec = new int[n + 1];int fib = fib(rec, n);return fib;}/*** 帶備忘錄的fib計算核心函數* * @param rec* @param n* @return*/private static int fib(int[] rec, int n) {if (n == 1 || n == 2) {return 1;}if (rec[n] > 0)return rec[n];rec[n] = fib(rec, n - 1) + fib(rec, n - 2);// 記錄return rec[n];}通過上面的例子我們可以發(fā)現:
如果一個問題可以被分解成若干個子問題,且這個子問題會重復出現,那就稱這個問題有重疊子問題(Overlapping Subproblems)
遞推寫法
例子:
數字三角
尋找一條從頂點到邊的路徑,使得經過的數字和最大
輸入:
triangle = {
{ 7 },
{ 3, 8 },
{ 8, 1, 0 },
{ 2, 7, 4, 4 },
{ 4, 5, 2, 6, 5 }};
輸出:
30
即(7 -> 3 ->8 ->7 ->5)
分析:
如果要求位置[0][0]到底層的最大和, 那么就要先求出它的兩個子問題:1.位置[1][0]到底層的最大和,與2.位置[1][1]到底層的最大和…(即走左邊還是走右邊)
即:
dp[0][0]=tri[0][0]+max(dp[0+1][0],dp[0+1][0+1]);dp[0][0] = tri[0][0] + max(dp[0 + 1][0], dp[0+1][0 + 1]); dp[0][0]=tri[0][0]+max(dp[0+1][0],dp[0+1][0+1]);
也即
dp[i][j]=tri[i][j]+max(dp[i+1][j],dp[i+1][j+1]);dp[i][j] = tri[i][j] + max(dp[i + 1][j], dp[i + 1][j + 1]); dp[i][j]=tri[i][j]+max(dp[i+1][j],dp[i+1][j+1]);
這里我們把dp[i][j]dp[i][j]dp[i][j]成為狀態(tài),把上面式子稱為狀態(tài)轉移方程
根據方程,我們從最底層(dp已知)開始不斷網上的求出最底層的dp值最后得到dp[0][0]dp[0][0]dp[0][0]就是答案
其中,dp已知的部分稱為邊界
顯然用遞歸也能實現上面的例子:
public static int digitalTriangle(int[][] tri) {int[][] rec = new int[tri.length - 1][tri[tri.length - 1].length - 1];for (int i = 0; i < rec.length; i++)Arrays.fill(rec[i], -1);int ans = core(rec, tri, 0, 0);Util.print(rec);System.out.println(cntOfRecCalled);return ans;}private static int core(int[][] rec, int[][] tri, int cur, int len) {if (len == tri.length - 1)return tri[len][cur];int ans = 0;if (rec[len][cur] != -1) {ans = rec[len][cur];} else {int left = core(rec, tri, cur, len + 1);int right = core(rec, tri, cur + 1, len + 1);ans = tri[len][cur] + Math.max(left, right);rec[len][cur] = ans;}return ans;}兩者的區(qū)別在于:
遞推寫法的計算方式是自底向上(Bottom-up Approach)
而遞歸的寫法是自頂向下(Top-down Approach)
通過上面的例子我們可以發(fā)現:
如果一個問題的最優(yōu)解可以由其子問題的最優(yōu)解構造出來,則稱該問題擁有最優(yōu)子結構(Optimal Substructure)
至此我們發(fā)現,一個問題只有具有重疊子問題或最優(yōu)子結構才能用動態(tài)規(guī)劃去解決,對于沒有被選擇的子問題,由于重疊子的存在,后面可能會再考慮到它們
動態(tài)規(guī)劃與分治區(qū)別
分治分解出的子問題不一定重疊, 分治解決的問題也不一定是最優(yōu)問題
什么是貪心算法
簡單說就是: 每一步都做出一個局部最優(yōu)的選擇, 最終的結果就是全局最優(yōu)
貪心算法(Greedy Algorithm, GA)是一種特殊的動態(tài)規(guī)劃即具有局部最優(yōu)解, 且具有貪心選擇性質的
如何使用貪心算法
貪心算法的關鍵在于找到貪心策略
舉個例子
現在有無數枚,1元,5元,10元,20元,50元,100元的鈔票,請用最少的張數湊出n(n>0)元錢
分析:
假如n=176, 要湊出176元,用一些面值較大的肯定比用面值較小的用的張數少
那么貪心策略就是,每次都選面值最大但不大于n的鈔票
寫出代碼如下:
/*** 用coins湊出n* * @param n 要湊的錢* @param coins 面值* @return 每種鈔票使用的張數*/public static int[] coins(int n, int[] coins) {int[] cnt = new int[coins.length];for (int i = coins.length - 1; i >= 0; i--) {int face = coins[i];if (face <= n) {int c = n / face;n = n - c * face;cnt[i] = c;}}return cnt;}// 測試public void testCoins() {int n = 176;int[] coins = { 1, 5, 10, 20, 50, 100 };int[] coins2 = coins(n, coins);System.out.println(Arrays.toString(coins2));}動態(tài)規(guī)劃與貪心算法區(qū)別
貪心算法采用自頂向下,但不會計算所有的子問題,而是使用貪心策略直接選擇一個子問題求解,沒有被選擇的子問題將不會被計算
而動態(tài)規(guī)劃會考慮所有的子問題
總結
- 上一篇: Java笔记--实时更新
- 下一篇: PTA 寻宝路线 (40 point(s