1. 程式人生 > >九章演算法高階班筆記6.動態規劃(下)

九章演算法高階班筆記6.動態規劃(下)

  1. 區間類DP
  • Stone Game
  • Burst Ballons
  • Scramble String
  1. 匹配類動規
  • Longest Common Subsequence
  • Edit Distance
  • K Edit Distance
  • Distinct Subquence
  • Interleaving String
  1. 揹包類DP
  • BackPackI
  • BackPackII
  • K SUM
  • Minimum Adjustment Cost

 

區間類Dp 

cs3k.com

特點:

  1. 求一段區間的解max/min/count
  2. 轉移方程通過區間更新
  3. 從大到小的更新

Coins in a Line III cs3k.com

There are n coins in a line. Two players take turns to take a coin from one of the ends of the line until there are no more coins left. The player with the larger amount of money wins.

Could you please decide the first player will win or lose?

Have you met this question in a real interview? Yes
Example
Given array A = [3,2,2], return true.

Given array A = [1,2,4], return true.

Given array A = [1,20,4], return false.

來來來, 先畫個圖:
Evernote Snapshot 20171105 151531 (1)

如圖, 我們發現, 一下相同的重複的[2], 可以用記憶花搜尋. 但是, 假設我們用一個狀態dp[1], 我們不知道剩的一個, 是2, 是3還是4啊. 因為現在取一個硬幣,可以從左邊取, 也可以從右邊取, 是有方向性的, 所以不能用dp[i]表示.
現在我們呢, 用一個區間的兩個下標表示

public class Solution {
    /** * @param values: an array of integers * @return: a boolean which equals to true if the first player will win */ public boolean firstWillWin(int[] values) { // write your code here int n = values.length; int[] sum = new int[n + 1]; sum[0] = 0; for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + values[i - 1]; // s[i][j] = sum[j + 1] - sum[i]; int[][] dp = new int[n][n]; for (int i = 0; i < n; ++i) dp[i][i] = values[i]; for (int len = 2; len <= n; ++len) { for (int i = 0; i < n; ++i) { int j = i + len - 1; if (j >= n) continue; int s = sum[j + 1] - sum[i]; dp[i][j] = Math.max(s - dp[i + 1][j], s - dp[i][j - 1]); } } return dp[0][n - 1] > sum[n] / 2; } } // 方法一 import java.util.*; public class Solution { /** * @param values: an array of integers * @return: a boolean which equals to true if the first player will win */ public boolean firstWillWin(int[] values) { // write your code here int n = values.length; int [][]dp = new int[n + 1][n + 1]; boolean [][]flag =new boolean[n + 1][n + 1]; int sum = 0; for(int now : values) sum += now; return sum < 2*MemorySearch(0,values.length - 1, dp, flag, values); } int MemorySearch(int left, int right, int [][]dp, boolean [][]flag, int []values) { if(flag[left][right]) return dp[left][right]; flag[left][right] = true; if(left > right) { dp[left][right] = 0; } else if (left == right) { dp[left][right] = values[left]; } else if(left + 1 == right) { dp[left][right] = Math.max(values[left], values[right]); } else { int pick_left = Math.min(MemorySearch(left + 2, right, dp, flag, values), MemorySearch(left + 1, right - 1, dp, flag, values)) + values[left]; int pick_right = Math.min(MemorySearch(left, right - 2, dp, flag, values), MemorySearch(left + 1, right - 1, dp, flag, values)) + values[right]; dp[left][right] = Math.max(pick_left, pick_right); } return dp[left][right]; } } // 方法二 import java.util.*; public class Solution { /** * @param values: an array of integers * @return: a boolean which equals to true if the first player will win */ public boolean firstWillWin(int[] values) { // write your code here int n = values.length; int [][]dp = new int[n + 1][n + 1]; boolean [][]flag =new boolean[n + 1][n + 1]; int[][] sum = new int[n + 1][n + 1]; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { sum[i][j] = i == j ? values[j] : sum[i][j-1] + values[j]; } } int allsum = 0; for(int now : values) allsum += now; return allsum < 2*MemorySearch(0,values.length - 1, dp, flag, values, sum); } int MemorySearch(int left, int right, int [][]dp, boolean [][]flag, int []values, int [][]sum) { if(flag[left][right]) return dp[left][right]; flag[left][right] = true; if(left > right) { dp[left][right] = 0; } else if (left == right) { dp[left][right] = values[left]; } else if(left + 1 == right) { dp[left][right] = Math.max(values[left], values[right]); } else { int cur = Math.min(MemorySearch(left+1, right, dp, flag, values, sum), MemorySearch(left,right-1, dp, flag, values, sum)); dp[left][right] = sum[left][right] - cur; } return dp[left][right]; } } 

我們會發現, 如圖

Evernote Snapshot 20171105 152835

我們是先初始化對角線, 然後往按區間往右上遞推的.先黃色的線, 然後藍色的線, 然後粉色的…

初始化, 就是初始化那些沒法遞推的.
從大往小想比較容易想, 可以用for來寫, 但是寫起來沒有遞迴單獨寫出一個方程來容易.

Stone Game 

cs3k.com

There is a stone game. At the beginning of the game the player picks n piles of stones in a line.

The goal is to merge the stones in one pile observing the following rules:

At each step of the game,the player can merge two adjacent piles to a new pile.
The score is the number of stones in the new pile.
You are to determine the minimum of the total score.
Example
For [4, 1, 1, 4], in the best solution, the total score is 18:

  1. Merge second and third piles => [4, 2, 4], score +2
  2. Merge the first two piles => [6, 4],score +6
  3. Merge the last two piles => [10], score +10

Other two examples:
[1, 1, 1, 1] return 8
[4, 4, 5, 9] return 43

不能用貪心

這道題不能用貪心, 就是永遠取剩下兩個數字裡面比較小的那兩個. 如圖:

6    4    4   6               4和4合併  +8
6       8     6               6和8合併  +14
14            6               14和6合併 +20
      20                      sum = 42

但是這是錯的, 因為正確的應該是:

6    4    4   6               6和4合併   +10
  10      4   6               4和6合併   +10
  10        10                10和10合併 +20
      20                      sum = 40

貪心錯誤的原因是: 合併的條件是相鄰, 而不是隨便挑最小的合併. 如果不要求相鄰, 那才是對的.

我們可以想到, 如圖所示, 列舉所有可能的情況

Evernote Snapshot 20171105 154248

搜尋是萬能的, 但是這道題用搜索, 就沒有可以合併的狀態了. 因為從小往大的搜尋, 是很難用記憶化搜尋做的. 我們可以反過來想, 從大往小. 最大的就是要求的, 然後我們倒著搜:
Evernote Snapshot 20171105 155157
如圖6.4所示, 這樣我們就發現, 誒有重複的了!! 可以記憶化搜尋啦!記憶化搜尋是由大拆小的過程.

死衚衕:容易想到的一個思路從小往大,列舉第一次合併是在哪?

記憶化搜尋的思路,從大到小,先考慮最後的0-N-1 合併的總花費

  1. State:
  • dp[i][j] 表示把第i到第j個石子合併到一起的最小花費
  1. Function:
  • 預處理sum[i,j] 表示i到j所有石子價值和
  • dp[i][j] = min(dp[i][k]+dp[k+1][j]+sum[i,j]) 對於所有k屬於{i,j}
  1. Intialize:
  • for each i
    • dp[i][i] = 0
  1. Answer:
  • dp[0][n-1]
public class Solution {
    /** * @param A an integer array * @return an integer */ int search(int l, int r, int[][] f, int[][] visit, int[][] sum) { if(visit[l][r] == 1) return f[l][r]; if(l == r) { visit[l][r] = 1; return f[l][r]; } f[l][r] = Integer.MAX_VALUE; for (int k = l; k < r; k++) { f[l][r] = Math.min(f[l][r], search(l, k, f, visit, sum) + search(k + 1, r, f, visit, sum) + sum[l][r]); } visit[l][r] = 1; return f[l][r]; } public int stoneGame(int[] A) { if (A == null || A.length == 0) { return 0; } int n = A.length; // initialize int[][] f = new int[n][n]; int[][] visit = new int[n][n]; for (int i = 0; i < n; i++) { f[i][i] = 0; } // preparation int[][] sum = new int[n][n]; for (int i = 0; i < n; i++) { sum[i][i] = A[i]; for (int j = i + 1; j < n; j++) { sum[i][j] = sum[i][j - 1] + A[j]; } } return search(0, n-1, f, visit, sum); } } // for 迴圈 public class Solution { /** * @param A an integer array * @return an integer */ public int stoneGame(int[] A) { // Write your code here if(A.length==0) { return 0; } int[][] dp=new int[A.length][A.length]; int[] sums=new int[A.length+1]; sums[0]=0; for(int i=0;i<A.length;i++){ for(int j=i;j<A.length;j++){ dp[i][j]=Integer.MAX_VALUE; } } for(int i=0;i<A.length;i++){ dp[i][i]=0; sums[i+1]=sums[i]+A[i]; } return search(0,A.length-1,dp,sums); } private int search(int start, int end, int[][] dp, int[] sums){ if(dp[start][end]!=Integer.MAX_VALUE){ return dp[start][end]; } int min=Integer.MAX_VALUE; for(int k=start;k<end;k++){ int left = search(start,k,dp,sums); int right = search(k+1,end,dp,sums); int now = sums[end+1]-sums[start]; min=Math.min(min,left+right+now); } dp[start][end]=min; return min; } } 

時間複雜度的話呢, 我們最好可以預處理sum, 這樣最後是O(n^3), 其中n^2
是遍歷, 剩下的n是每個位置的移動.

Burst Balloons

cs3k.com

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.

Find the maximum coins you can collect by bursting the balloons wisely.

  • You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
  • 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

Have you met this question in a real interview? Yes
Example
Given [4, 1, 5, 10]
Return 270

nums = [4, 1, 5, 10] burst 1, get coins 4 * 1 * 5 = 20
nums = [4, 5, 10] burst 5, get coins 4 * 5 * 10 = 200
nums = [4, 10] burst 4, get coins 1 * 4 * 10 = 40
nums = [10] burst 10, get coins 1 * 10 * 1 = 10

Total coins 20 + 200 + 40 + 10 = 270

我們可以用圖6.5的方法, 列舉第一次吹爆哪個氣球, 然後求解:
Evernote Snapshot 20171105 155243

我們同時