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

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

大綱:

cs3k.com

  1. 滾動陣列
  • House Robber I/II
  • Maximal Square
  1. 記憶化搜尋
  • Longest Increasing Subsequence
  • Coin in a line I/II/III

 

什麼是動態規劃? cs3k.com

動態規劃是解決重複子問題的一種方法。

動態規劃四要素

cs3k.com

  1. 狀態 State
  • 靈感,創造力,儲存小規模問題的結果
  • 最優解/Maximum/Minimum
  • Yes/No 存在不存在滿足條件的答案
  • Count(*) 有多少個可行解
  1. 方程 Function
  • 狀態之間的聯絡,怎麼通過小的狀態,來求得大的狀態
  1. 初始化 Intialization
  • 最極限的小狀態是什麼, 起點
  1. 答案 Answer
  • 最大的那個狀態是什麼,終點

狀態的六大問題:

cs3k.com

cs3k.com

  1. 座標型15%
     jump game: 棋盤,格子
     f[i]代表從起點走到i座標
    
  2. 序列型30%
    f[i]代表前i個元素總和,i=0表示不取任何元素
    
  3. 雙序列型30%
  4. 劃分型10%
  5. 揹包型10%
  6. 區間型5%

滾動陣列優化

f[i] = max(f[i-1], f[i-2] + A[i

cs3k.com

]);

轉換為

f[i%2] = max(f[(i-1)%2]和 f[(i-2)%2])

House Robber

cs3k.com

cs3k.com

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Given [3, 8, 4], return 8.

這是一道典型的序列型的動態規劃。
f[i]代表路過完第i家的時候, 最多能有多少錢。
一般求什麼,f[i]就是什麼。
對於i=n的時候,我們有兩個選擇, 就是搶或者不搶:

搶的話,最後是i=n-2的錢數加上第n家的錢數
不搶的話, 錢數等於i=n-1的錢數

說著感覺沒啥問題,但是總覺得隱隱約約哪裡不對勁, 漏了什麼情況, 於是乎我看了個部落格http://blog.csdn.net/zsy112371/article/details/52541925上面用此段程式碼做解釋

帶碼引用 https://discuss.leetcode.com/topic/11082/java-o-n-solution-space-o-1:

public int rob(int[] num) {  
    int[][] dp = new int[num.length + 1][2];  
    for (int i = 1; i <= num.length; i++) {  
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);  
        dp[i][1] = num[i - 1] + dp[i - 1][0];  
    }  
    return Math.max(dp[num.length][0], dp[num.length][1]);  
}  

解釋引用自部落格http://blog.csdn.net/zsy112371/article/details/52541925

稍微解釋一下這個程式碼,dp這個二維陣列代表盜竊前i棟房子帶來的最大收益,其中第二維一共有兩個選擇分別是0和1。0代表不盜竊第i棟房子,1代表盜竊第i棟房子。換句話就是說,dp[i][0]代表盜竊前i棟房子的最大收益,但是不包括第i棟房子的(因為沒有盜竊第i棟房子),而dp[i][0]代表盜竊前i棟房子的最大收益,其中包括了第i棟房子的(因為第i棟房子被盜竊了)。
其實對一棟房子來說,結果無非是兩種,被盜竊和沒被竊。所以說,才會有之前分0和1兩種情況進行討論。如果第i棟房子沒被盜竊的話,那麼dp[i][0] = dp[i-1][0]和dp[i-1][1]中的最大值。這個比較好理解,如果第i棟房子沒被竊,那麼最大總收益dp[i][0]一定和dp[i-1][0],dp[i-1][1]這兩個之中最大的相同。而假若第i棟房子被竊,那麼dp[i][1]一定等於第num[i-1](注意,這裡的i是從1開始的,所以i-1正好對應num中的第i項,因為num中的index是從0開始的)+dp[i-1][0],因為第i棟房子已經被竊了,第i-1棟房子肯定不能被竊,否則會觸發警報,所以我們只能取dp[i-1][0]即第i-1棟房子沒被竊情況的最大值。迴圈結束,最後返回dp[num.length][0]和dp[nums.length][1]較大的一項。
結束來自http://blog.csdn.net/zsy112371/article/details/52541925 的引用。

我們走幾個小栗子:

陣列[80, 7, 9, 90, 87]按上面的做法得到每步的結果, 選i和不選i的結果

       80      7      9      90      87
不選i   0     (80)    80      89     80+90
選i   (80)     7     (89)   (80+90) (87+89)     

換點數字和順序:

       80      9      7       3      60
不選i   0     (80)    80      87      87
選i   (80)     9     (87)   (80+3) (87+60)      

我們發現, 對於每一個i, 影響後面能用到的, 只有選i和不選i兩個決策結果中比較大的那個, 所以我們可以只存一個, f[i]代表路過完第i家的時候, 最多能有多少錢. 就是以下演算法:

public class Solution {
    /**
     * @param A: An array of non-negative integers.
     * return: The maximum amount of money you can rob tonight
     */
    //---方法一---
    public long houseRobber(int[] A) {
        // write your code here
        int n = A.length;
        if(n == 0)
            return 0;
        long []res = new long[n+1];
        
        
        res[0] = 0;
        res[1] = A[0];
        for(int i = 2; i <= n; i++) {
            res[i] = Math.max(res[i-1], res[i-2] + A[i-1]);
        }
        return res[n];
    }

之後呢, 我們發現, 對於某個狀態c,假設它是奇數位的狀態, 它前面有奇狀態a和偶狀態b:

 ...  a       b       c  ...
 ... 奇1     偶1      奇2  ...

它的狀態只和它前面的一個奇狀態和一個偶狀態有關, 所以我們可以只存三個變數:

         奇1 和 偶1 推出 奇2

可是由於在奇2狀態結果一出現的時候, 奇1結果就沒啥用了, 可以被立即替換, 所以我們就可以只存一對奇偶的狀態, 即兩個變數, 如下:

    public long houseRobber(int[] A) {
        // write your code here
        int n = A.length;
        if(n == 0)
            return 0;
        long []res = new long[2];
        
        
        res[0] = 0;
        res[1] = A[0];
        for(int i = 2; i <= n; i++) {
            res[i%2] = Math.max(res[(i-1)%2], res[(i-2)%2] + A[i-1]);
        }
        return res[n%2];
    }
}

這就是滾動陣列, 或者叫做滾動指標的空間優化.

House Robber II

cs3k.com

After robbing those houses on that street, the thief has found himself a new place for his thievery so that he will not get too much attention. This time, all houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, the security system for these houses remain the same as for those in the previous street.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example
nums = [3,6,4], return 6

現在呢, 我們如果選3了的話, 4不好搞。

如果選4了的話呢, 3不好搞。
這就變成了一個迴圈陣列問題, 迴圈陣列問題有三種方法可以解:

  1. 取反
  2. 分裂
  3. 倍增

這裡我們用分裂的方法, 把陣列

[3, 6, 4]

分成, 選3的:

[3, 6]

和不選3的:

[6, 4]

然後把這兩個非迴圈陣列分別用上面的方法求解.
我猜這可能是雙序列動規吧…

public class Solution {
    public int houseRobber2(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }
        return Math.max(robber1(nums, 0, nums.length - 2), robber1(nums, 1, nums.length - 1));
    }
    public int robber1(int[] nums, int st, int ed) {
        int []res = new int[2];
        if(st == ed) 
            return nums[ed];
        if(st+1 == ed)
            return Math.max(nums[st], nums[ed]);
        res[st%2] = nums[st];
        res[(st+1)%2] = Math.max(nums[st], nums[st+1]);
        
        for(int i = st+2; i <= ed; i++) {
            res[i%2] = Math.max(res[(i-1)%2], res[(i-2)%2] + nums[i]);
            
        }
        return res[ed%2];
    }
}

Maximal Square

cs3k.com

Given a 2D binary matrix filled with 0’s and 1’s, find the largest square containing all 1’s and return its area.

Example
For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 4.

長方形往往選左上和右下角的角點開始, 正方形一般選右下角.

這道題比較直白的做法是這樣的:

for x = 0 ~ n
 for y = 0 ~ m
  for a = 1 ~ min(m,n)
   for (x = x ~ x+a)
    for (y = y ~ y+1)

時間複雜度是O( m * n * min(m,n) * m * n)
這個時間複雜度, 真是不小. 接著我們想想, 發現呢, (2,3)這個點為右下角點的square的大小, 其實和(1,2)有關, 它最大是(1,2)的squre的邊長再加一
那麼呢, 我們可以把正方形分成pic5.1圖中的幾個部分:
Evernote Snapshot 20171031 162349
對於一個點A, 我們需要看看它左上的點最大的正方形邊長, 然後驗證它左邊連續是1的點的長度和上面連續是1的點的長度, 即

1. for向左都是1的長度
2. for向上都是1的長度
3. for左上點的最大正方形的邊長

1, 2和3的值中的最小值+1就是結果

其中1和2可以用預處理過得矩陣left[][]和up[][]優化.
這是狀態方程是:

if matrix[i][j] == 1
    f[i][j] = min(LEFT[i][j-1], UP[i-1][j], f[i-1][j-1]) + 1;
if matrix[i][j] == 0
    f[i][j] = 0

接著呢, 我們發現, 其實left和up矩陣是不需要的. 如圖pic5.2:
Evernote Snapshot 20171031 164120

我們發現, A為右下角點的鉛筆畫大正方形A全為1需要的條件是:

綠色的B, 黑色的C和粉色的D全都為1

加上

並且A自己的最右下角點為1

所以我們只需要在左點, 上點和左上點各自最大正方形的邊長取最小值, 再加1就行了. 狀態方程變成:

if matrix[i][j] == 1
     f[i][j] = min(f[i - 1][j], f[i][j-1], f[i-1][j-1]) + 1;
if matrix[i][j] == 0
     f[i][j] = 0
public class Solution {
    /**
     * @param matrix: a matrix of 0 and 1
     * @return: an integer
     */
    public int maxSquare(int[][] matrix) {
        // write your code here
        int ans = 0;
        int n = matrix.length;
        int m;
        if(n > 0)
            m = matrix[0].length;
        else 
            return ans;
        int [][]res = new int [n][m];
        for(int i = 0; i < n; i++){
            res[i][0] = matrix[i][0];
            ans = Math.max(res[i][0] , ans);
            for(int j = 1; j < m; j++) {                 if(i > 0) {
                    if(matrix[i][j] > 0) {
                        res[i][j] = Math.min(res[i - 1][j],Math.min(res[i][j-1], res[i-1][j-1])) + 1;
                    } else {
                        res[i][j] = 0;
                    }
                    
                }
                else {
                    res[i][j] = matrix[i][j];
                }
                ans = Math.max(res[i][j], ans);
            }
        }
        return ans*ans;
    }
}

那動態規劃中什麼要初始化呢?

cs3k.com

動態方程不能求的要初始化

滾動陣列優化

需要多少個狀態, 就存多少個.

求第i個狀態, 如果只與i-1有關, 那就只需要兩個陣列. 用previous陣列, 推now陣列.
所以上一個解法中, j可以都模2.

那i為什麼不能模呢?
因為如果i模了的話, 下一行左邊開始的時候, 存的還是上一行最右邊末尾的值, 不行噠~ 狀態方程變為:

if matrix[i][j] == 1
    f[i%2][j] = min(f[(i - 1)%2][j], f[i%2][j-1], f[(i-1)%2][j-1]) + 1;
if matrix[i][j] == 0
    f[i%2][j] = 0

其中要注意初始化和答案也要記得模:

初始化 Intialization:

f[i%2][0] = matrix[i][0];
f[0][j] = matrix[0][j];

答案 Answer

max{f[i%2][j]}

Follow Up Maximal Square II

cs3k.com

Given a 2D binary matrix filled with 0’s and 1’s, find the largest square which diagonal is all 1 and others is 0.

Notice

Only consider the main diagonal situation.

For example, given the following matrix:

1 0 1 0 0
1 0 0 1 0
1 1 0 0 1
1 0 0 1 0
Return 9

這時候up和left陣列就不能省啦.

public class Solution {
    /**
     * @param matrix a matrix of 0 and 1
     * @return an integer
     */
    public int maxSquare2(int[][] matrix) {
        // write your code here
        int n = matrix.length;
        if (n == 0)
            return 0;

        int m = matrix[0].length;
        if (m == 0)
            return 0;

        int[][] f = new int[n][m];
        int[][] u = new int[n][m];
        int[][] l = new int[n][m];

        int length = 0;
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < m; ++j) {                 if (matrix[i][j] == 0) {                     f[i][j] = 0;                     u[i][j] = l[i][j] = 1;                     if (i > 0)
                        u[i][j] = u[i - 1][j] + 1;
                    if (j > 0)
                        l[i][j] = l[i][j - 1] + 1;
                } else {
                    u[i][j] = l[i][j] = 0;
                    if (i > 0 && j > 0)
                        f[i][j] = Math.min(f[i - 1][j - 1], Math.min(u[i - 1][j], l[i][j - 1])) + 1;
                    else 
                        f[i][j] = 1;
                }
                length = Math.max(length, f[i][j]);
            }
        return length * length;
    }
}

一維的叫滾動陣列, 二位的叫滾動矩陣, 二維動態規劃空間優化的特點是:

  1. f[i][j] = 由f[i-1]行 來決定狀態,
  2. 第i行跟 i-1行之前毫無關係,
  3. 所以狀態轉變為
     f[i%2][j] = 由f[(i-1)%2]行來決定狀態
    

記憶化搜尋

cs3k.com

  1. 記憶搜尋本質是:動態規劃
  • 動態規劃就是解決了重複計算的搜尋
  1. 動態規劃的實現方式:
  • 迴圈(從小到大遞推)
  • 記憶化搜尋(從大到小搜尋)
  1. 記憶化搜尋
  • 畫搜尋樹
  • 萬金油搜尋可以解決一切問題
    記憶化搜尋可以解決一切動態規劃的問題
    動態規劃都可以用記憶化搜尋解決, 但是記憶化搜尋不一定是最優解

Longest Increasing Continuous Subsequence

cs3k.com

Give an integer array,find the longest increasing continuous subsequence in this array.

An increasing continuous subsequence:

Can be from right to left or from left to right.
Indices of the integers in the subsequence should be continuous.
Notice

O(n) time and O(1) extra space.

Example
For [5, 4, 2, 1, 3], the LICS is [5, 4, 2, 1], return 4.

For [5, 1, 2, 3, 4], the LICS is [1, 2, 3, 4], return 4.

這道題:

如果a[i] > a[i-1]時,f[i] = f[i-1] + 1
如果a[i] < a[i-1]呢,f[i] = 1

Longest Increasing Continuous subsequence II

cs3k.com

Give you an integer matrix (with row size n, column size m),find the longest increasing continuous subsequence in this matrix. (The definition of the longest increasing continuous subsequence here can start at any row or column and go up/down/right/left any direction).

Example
Given a matrix:

[
[1 ,2 ,3 ,4 ,5],
[16,17,24,23,6],
[15,18,25,22,7],
[14,19,20,21,8],
[13,12,11,10,9]
]
return 25

類似與滑雪問題
這道題很容易想到二維dp, 記錄f[i][j] 是以i,j為結尾的LIS。但是由於我們走的方向不僅僅是從左往右和從上往下, 還可能從右往左和從下往上, 所以

1. 轉化方程式非順序性的(順序指的是從左往右,從上往下)
2. 初始化的狀態在哪兒是確定的

這樣的問題, 我們只能dfs加記憶化搜尋.
每個位置最長的LIS記錄在一個矩陣dp[][] 裡面, 同時用一個flag[][] 矩陣記錄下遍歷過沒有. dp和flag矩陣可以寫在一起, 但是最好不要, 因為dp代表當前最長長度, flag代表遍歷過沒有, 意義不同.
這道題的時間複雜度是O(n*n), 因為有flag, 每個點最多遍歷一次.

public class Solution {
    /**
     * @param A an integer matrix
     * @return  an integer
     */
    int [][]dp;
    int [][]flag ;
    int n ,m;
    public int longestIncreasingContinuousSubsequenceII(int[][] A) {
        // Write your code here
        if(A.length == 0)
            return 0;
        n = A.length;
         m  = A[0].length;
        int ans= 0;
        dp = new int[n][m];
        flag = new int[n][m];
        
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) { 
                dp[i][j] = search(i, j, A);
                ans = Math.max(ans, dp[i][j]);
            }
        }
        return ans;
    }
    int []dx = {1,-1,0,0};
    int []dy = {0,0,1,-1};
    
    int search(int x, int y, int[][] A)   {
        if(flag[x][y] != 0)    
            return dp[x][y];
        
        int ans = 1; 
        int nx , ny;
        for(int i = 0; i < 4; i++) {
            nx = x + dx[i];
            ny = y + dy[i];
            if(0<= nx && nx < n && 0<= ny && ny < m ) {                 if( A[x][y] > A[nx][ny]) {
                    ans = Math.max(ans,  search( nx, ny, A) + 1);
                }
            }
        }
        flag[x][y] = 1;
        dp[x][y] = ans;
        return ans;
    }
}

什麼時候用記憶化搜尋呢?

cs3k.com

  1. 狀態轉移特別麻煩, 不是順序性
  2. 初始化狀態不是特別容易找到

這時候我們用萬能的dfs加memorization
enter image description here

那怎麼根據DP四要素轉化為記憶化搜尋呢?

  1. State:
  • dp[x][y] 以x,y作為結尾的最長子序列
  1. Function:
  • 遍歷x,y 上下左右四個格子
    dp[x][y] = dp[nx][ny] + 1
         (if a[x][y] > a[nx][ny])
    
  1. Intialize:
  • dp[x][y] 是極小值時,初始化為1
  1. Answer:
  • dp[x][y]中最大值

enter image description here

記憶化搜尋的博弈類問題

cs3k.com

Coins in a Line

There are n coins in a line. Two players take turns to take one or two coins from right side until there are no more coins left. The player who take the last coin wins.

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

Example
n = 1, return true.

n = 2, return true.

n = 3, return false.

n = 4, return true.

n = 5, return true.

棋盤和玩遊戲的問題一定是博弈類問題.

這個問題我直接就想到除以三看餘數, 但是老師說這個方法不好想…並且沒有通用性. 我覺得我可能思維詭異吧, 好多我自己能繞一個小時把自己都繞暈了的題, 老師說這個大家都應該能想到吧…

來來來, 不能用貪心我們就走個小栗子看看,o代表一個硬幣

                     ooooo
        先手拿       1 /        \ 2
                   oooo        ooo
        後手拿    1/    \2     1/  \2
                ooo    oo     oo    o
        先手拿 先手輸  先手贏  先手贏 先手贏

其中剩兩個石頭的情況有兩次,我們可以判斷一次然後存下來以備後用。

首先, 我們知道輪到先手, 剩4個,2個或1個石頭的時候,是贏的. 而3個石頭的時候, 是輸的。所以對於5個石頭的情況, 我們要保證先手無論在對方拿了1個剩四個還是拿了2個剩3個的情況下,都有機會贏。
可以歸納為以下方法:

  1. State:
  • dp[i] 現在還剩i個硬幣,現在先手取硬幣的人最後輸贏狀況
  1. Function:
  • dp[i] = (dp[i-2]&& dp[i-3]) || (dp[i-3]&& dp[i-4] )
  1. Intialize:
  • dp[0] = false
  • dp[1] = true
  • dp[2] = true
  • dp[3] = false
  1. Answer:
  • dp[n]

此外還有另一個方法:

  1. State:
  • dp[i] 現在還剩i個硬幣,現在當前取硬幣的人最後輸贏狀況
  1. Function:
  • dp[i] = true (dp[i-1]==false 或者 dp[i-2]==false)
  1. Intialize:
  • dp[0] = false
  • dp[1] = true
  • dp[2] = true
  1. Answer:
  • dp[n]
public class Solution {
    /**
     * @param n: an integer
     * @return: a boolean which equals to true if the first player will win
     */
    public boolean firstWillWin(int n) {
        // write your code here
        int []dp = new int[n+1];
        
        return MemorySearch(n, dp);
        
    }
    boolean MemorySearch(int n, int []dp) { // 0 is empty, 1 is false, 2 is true
        if(dp[n] != 0) {
            if(dp[n] == 1)
                return false;
            else
                return true;
        }
        if(n <= 0) {
            dp[n] = 1;
        } else if(n == 1) {
            dp[n] = 2;
        } else if(n == 2) {
            dp[n] = 2;
        } else if(n == 3) {
            dp[n] = 1;
        } else {
            if((MemorySearch(n-2, dp) && MemorySearch(n-3, dp)) || 
                (MemorySearch(n-3, dp) && MemorySearch(n-4, dp) )) {
                dp[n] = 2;
            } else {
                dp[n] = 1;
            }
        }
        if(dp[n] == 2) 
            return true;
        return false;
    }
}

// 方法二 StackOverflow
public class Solution {
    /**
     * @param n: an integer
     * @return: a boolean which equals to true if the first player will win
     */
    public boolean firstWillWin(int n) {
        // write your code here
        boolean []dp = new boolean[n+1];
        boolean []flag = new boolean[n+1];
        return MemorySearch(n, dp, flag);
    }
    boolean MemorySearch(int i, boolean []dp, boolean []flag) {
        if(flag[i] == true) {
            return dp[i];
        }
        if(i == 0) {
            dp[i] = false;
        } else if(i == 1) {
            dp[i] = true;
        } else if(i == 2) {
            dp[i] = true;
        } else {
            dp[i] = !MemorySearch(i-1, dp, flag) || !MemorySearch(i-2, dp, flag);
        }
        flag[i] =true;
        return dp[i];
    }
}

//方法三
public class Solution {
    /**
     * @param n: an integer
     * @return: a boolean which equals to true if the first player will win
     */
    public boolean firstWillWin(int n) {
        // write your code here
        if (n == 0)
            return false;
        else if (n == 1)
            return true;
        else if (n == 2)
            return true;
            
        boolean []dp = new boolean[n+1];
        dp[0] = false;
        dp[1] = true;
        dp[2] = true;
        for (int i = 3; i <= n; i++) 
            dp[i] = !dp[i - 1] || !dp[i - 2];
            
        return dp[n];
    }
}

Coins in a Line II

There are n coins with different value in a line. Two players take turns to take one or two coins from left side until there are no more coins left. The player who take the coins with the most value wins.

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

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

Given A = [1,2,4], return false.

來來來, 走個栗子:

                      [5,1,2,10] 
        先手拿       1(5) /        \ 2(6) 
                    [1,2,10]       [2,10]  
        後手拿   1(1)/    \2(3)  1(2)/  \2(12)
              [2,10]  [10]        [10] [] 

每次拿硬幣的原則不是當前手拿的最多, 而是加上剩下能拿的總共最多。

f[i]先手還剩i個硬幣:

f[5] 左 = 5 + min(f[2],f[1])
     右 = 6 + min(f[1],f[0])
  1. State:
  • dp[i] 現在還剩i個硬幣,現在先手取硬幣的人最後最多取硬幣價值
  1. Function:
  • i 是所有硬幣數目
  • coin[n-i] 表示倒數第i個硬幣
  • dp[i] = max(min(dp[i-2], dp[i-3])+coin[n-i] ),(min(dp[i-3],dp[i-4])+coin[n-i]+coin[n-i+1] )
  1. Intialize:
  • dp[0] = 0
  • dp[1] = coin[i-1]
  • dp[2] = coin[i-2] + coin[i-1]
  • dp[3] = coin[i-2] + coin[i-3]
  1. Answer:
  • dp[n] > sum/2

另一種方法:

  1. State:
  • dp[i] 現在還剩i個硬幣,現在當前取硬幣的人最後最多取硬幣價值
  1. Function:
  • i 是所有硬幣數目
  • sum[i] 是後i個硬幣的總和
  • dp[i] = max(sum[i]-dp[i-1], sum[i] – dp[i-2])
  1. Intialize:
  • dp[0] = 0
  • dp[1] = coin[i-1]
  • dp[2] = coin[i-2] + coin[i-1]
  1. Answer:
  • dp[n]
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];
        for (int i = 1; i <= n; ++i)
            sum[i] = sum[i -  1] + values[n - i];

        int[] dp = new int[n + 1];
        dp[1] = values[n - 1];
        for (int i = 2; i <= n; ++i)             dp[i] = Math.max(sum[i] - dp[i - 1], sum[i] - dp[i - 2]);                      return dp[n]  > 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 []dp = new int[values.length + 1];
        boolean []flag =new boolean[values.length + 1];
        int sum = 0;
        for(int now : values) 
            sum += now;
        
        return sum < 2*MemorySearch(values.length, dp, flag, values);     }     int MemorySearch(int n, int []dp, boolean []flag, int []values) {          if(flag[n] == true)             return dp[n];         flag[n] = true;         if(n == 0)  {             dp[n] = 0;           } else if(n == 1) {             dp[n] = values[values.length-1];         } else if(n == 2) {             dp[n] = values[values.length-1] + values[values.length-2];          } else if(n == 3){             dp[n] = values[values.length-2] + values[values.length-3];          } else {             dp[n] = Math.max(                 Math.min(MemorySearch(n-2, dp, flag,values) , MemorySearch(n-3, dp, flag, values)) + values[values.length-n],                 Math.min(MemorySearch(n-3, dp, flag, values), MemorySearch(n-4, dp, flag, values)) + values[values.length-n] + values[values.length - n + 1]                 );         }              return dp[n];     }     } // 方法二 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];         boolean []flag =new boolean[n + 1];         int []sum = new int[n+1];         int allsum = values[n-1];         sum[n-1] = values[n-1];         for(int i = n-2; i >= 0; i--) { 
            sum[i] += sum[i+1] + values[i];
            allsum += values[i];
        }
        return allsum/2 < MemorySearch(0, n, dp, flag, values, sum);
    }
    int MemorySearch(int i, int n, int []dp, boolean []flag, int []values, int []sum) {
        if(flag[i] == true)
            return dp[i];
        flag[i] = true;
        if(i == n)  {
            dp[n] = 0;  
        } else if(i == n-1) {
            dp[i] = values[i];
        } else if(i == n-2) {
            dp[i] = values[i] + values[i + 1]; 
        } else {
            dp[i] = sum[i] -
                Math.min(MemorySearch(i+1, n, dp, flag, values, sum) , MemorySearch(i+2, n, dp, flag, values, sum));
        }
        return dp[i];
    }
   
}

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?

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.

方法一:

  1. State:
  • dp[i][j] 現在還第i到第j的硬幣,現在先手取硬幣的人最後最多取硬幣價值
  1. Function:
  • left = min(dp[i+2][j], dp[i+1][j-1])+coin[i] [i+1,j]
  • right = min(dp[i][j-2], dp[i+1][j-1])+coin[j] [i,j-1]
  • dp[i][j] = max(left, right).
  1. Intialize:
  • dp[i][i] = coin[i],
  • dp[i][i+1] = max(coin[i],coin[i+1]),
  1. Answer:
  • dp[0][n-1] > sum/2

方法二:

  1. State:
  • dp[i][j] 現在還第i到第j的硬幣,現在當前取硬幣的人最後最多取硬幣價值
  1. Function:
  • sum[i][j]第i到第j的硬幣價值總和
  • dp[i][j] = max(sum[i][j] – dp[i+1][j], sum[i][j] – dp[i][j-1]);
  1. Intialize:
  • dp[i][i] = coin[i],
  1. Answer:
  • dp[0][n-1]
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];   
    }
}

什麼時候用記憶化搜尋?

cs3k.com

  1. 狀態轉移特別麻煩,不是順序性。
  • Longest Increasing continuous Subsequence 2D
    • 遍歷x,y 上下左右四個格子 dp[x][y] = dp[nx][ny]
  • Coins in a Line III
    • dp[i][j] = sum[i][j] – min(dp[i+1][j], dp[i][j-1]);
  1. 初始化狀態不是很容易找到
  • Stone Game
    • 初始化dp[i][i] = 0
  • Longest Increasing continuous Subsequence 2D
    • 初始化極小值
  1. 從大到小