【leetcode】741 摘櫻桃(動態規劃)

zjwreal發表於2019-06-14

題目連結:https://leetcode-cn.com/problems/cherry-pickup/

題目描述

一個N x N的網格(grid) 代表了一塊櫻桃地,每個格子由以下三種數字的一種來表示:

  • 0 表示這個格子是空的,所以你可以穿過它。

  • 1 表示這個格子裡裝著一個櫻桃,你可以摘到櫻桃然後穿過它。

  • -1 表示這個格子裡有荊棘,擋著你的路。

你的任務是在遵守下列規則的情況下,儘可能的摘到最多櫻桃:

  • 從位置 (0, 0) 出發,最後到達 (N-1, N-1) ,只能向下或向右走,並且只能穿越有效的格子(即只可以穿過值為0或者1的格子);
  • 當到達 (N-1, N-1) 後,你要繼續走,直到返回到 (0, 0) ,只能向上或向左走,並且只能穿越有效的格子;
  • 當你經過一個格子且這個格子包含一個櫻桃時,你將摘到櫻桃並且這個格子會變成空的(值變為0);
  • 如果在 (0, 0) 和 (N-1, N-1) 之間不存在一條可經過的路徑,則沒有任何一個櫻桃能被摘到。

示例 1:

輸入: grid =
[[0, 1, -1],
 [1, 0, -1],
 [1, 1,  1]]
輸出: 5
解釋: 
玩家從(0,0)點出發,經過了向下走,向下走,向右走,向右走,到達了點(2, 2)。
在這趟單程中,總共摘到了4顆櫻桃,矩陣變成了[[0,1,-1],[0,0,-1],[0,0,0]]。
接著,這名玩家向左走,向上走,向上走,向左走,返回了起始點,又摘到了1顆櫻桃。
在旅程中,總共摘到了5顆櫻桃,這是可以摘到的最大值了。

說明:

  • grid 是一個 N * N 的二維陣列,N的取值範圍是1 <= N <= 50。
  • 每一個 grid[i][j] 都是集合 {-1, 0, 1}其中的一個數。
  • 可以保證起點 grid[0][0] 和終點 grid[N-1][N-1] 的值都不會是 -1。

思路

題目的詳解見英文leetcode原帖:https://leetcode.com/problems/cherry-pickup/discuss/109903/Step-by-step-guidance-of-the-O(N3)-time-and-O(N2)-space-solution

因為題目限制了grid[i][j]=1的節點只能訪問一次,所以核心的問題在於要避免partI和partII重複計數。

I 暴力嘗試

暴力回溯,指數級時間複雜度。每趟來回 (4N-4)步,可能的往返數 2^(4N-4)

II 動態規劃嘗試

(0, 0) ==> (N-1, N-1)(N-1, N-1)==>(0,0)分別使用動態規劃得到其子問題最優解,但是總體 (0, 0) ==> (N-1, N-1) ==> (0, 0) 不一定是最優解。

grid = [[1,1,1,0,1],
        [0,0,0,0,0],
        [0,0,0,0,0],
        [0,0,0,0,0],
        [1,0,1,1,1]].

如上式在partI可以得到最優解,(0, 0) ==> (0, 2) ==> (4, 2) ==> (4, 4)總和為6,對應的partII最多總和為1,總體來回總和為7。然後沿著矩形邊緣可以得到所有的8個櫻桃。所以這個貪心策略不一定能得到最優解。

III 修改grid矩陣的動態規劃嘗試

記錄當前grid的狀態,空間複雜度太高了

IV 最終版-不修改grid矩陣的動態規劃

是否可以縮短路程,不需要走到右下角?YES!

我們重新定義 T(i,j) 為簡化路程 (0, 0) ==> (i, j) ==> (0, 0)可以得到的最大數,無需修改輸入矩陣。此時原問題可以表示為T(N-1,N-1)。為了得到遞推關係式:

對於每個座標 (i,j),我們有兩種方式到達以及兩種方式離開該點:(i-1, j)(i, j-1),往返路程可以分為以下四個case:

Case 1: (0, 0) ==> (i-1, j) ==> (i, j) ==> (i-1, j) ==> (0, 0)
Case 2: (0, 0) ==> (i, j-1) ==> (i, j) ==> (i, j-1) ==> (0, 0)
Case 3: (0, 0) ==> (i-1, j) ==> (i, j) ==> (i, j-1) ==> (0, 0)
Case 4: (0, 0) ==> (i, j-1) ==> (i, j) ==> (i-1, j) ==> (0, 0)

根據定義,case1等價於T(i-1, j) + grid[i][j] ,case2等價於T(i, j-1) + grid[i][j]。但是我們定義的T(i, j)並沒有覆蓋最後兩種情形:PartI最後一步和PartII第一步不同。因此我們需要修改T(i,j)定義,擴充套件為T(i, j, p, q),表明兩段路程(0, 0) ==> (i, j); (p, q) ==> (0, 0)的最大櫻桃數,無需修改gird矩陣。

類似上文所述,我們有兩種方式到達座標(i,j),兩種方式離開座標(p,q)

Case 1: (0, 0) ==> (i-1, j) ==> (i, j); (p, q) ==> (p-1, q) ==> (0, 0)
Case 2: (0, 0) ==> (i-1, j) ==> (i, j); (p, q) ==> (p, q-1) ==> (0, 0)
Case 3: (0, 0) ==> (i, j-1) ==> (i, j); (p, q) ==> (p-1, q) ==> (0, 0)
Case 4: (0, 0) ==> (i, j-1) ==> (i, j); (p, q) ==> (p, q-1) ==> (0, 0)

根據定義得到:

Case 1 is equivalent to T(i-1, j, p-1, q) + grid[i][j] + grid[p][q];
Case 2 is equivalent to T(i-1, j, p, q-1) + grid[i][j] + grid[p][q];
Case 3 is equivalent to T(i, j-1, p-1, q) + grid[i][j] + grid[p][q];
Case 4 is equivalent to T(i, j-1, p, q-1) + grid[i][j] + grid[p][q];

遞推關係式為:

T(i, j, p, q) = grid[i][j] + grid[p][q] + max{T(i-1, j, p-1, q), T(i-1, j, p, q-1), T(i, j-1, p-1, q), T(i, j-1, p, q-1)}

約束條件-避免重複計數

至此,我們需要設定約束來避免對同一格子重複計數。因為上文我們已經在計算T(i, j, p, q)時計數了gird[i][j]gird[p][q],為了避免重複計數,這兩個gird節點不應該在 T(i-1, j, p-1, q), T(i-1, j, p, q-1), T(i, j-1, p-1, q) and T(i, j-1, p, q-1)這四個中任何一個的計算中被計數。

顯然 (i, j) 不可能出現在 (0, 0) ==> (i-1, j)(0, 0) ==> (i, j-1),同理 (p, q) 不會出現在 (p-1, q) ==> (0, 0)(p, q-1) ==> (0, 0)

因此如果我們可以保證(i, j)不出現在(p-1, q) ==> (0, 0) or (p, q-1) ==> (0, 0) 並且 (p, q) 不出現在 (0, 0) ==> (i-1, j) or (0, 0) ==> (i, j-1),則將不會發生重複計數。怎麼做呢?

(0, 0) ==> (i-1, j) and (0, 0) ==> (i, j-1)舉例。我們知道這些路徑的邊界,前者所有路徑落在矩形 [0, 0, i-1, j] ,後者所有路徑落在矩形[0, 0, i, j-1],這表明兩個路徑合起來將會落在矩形[0, 0, i, j]除右下角(i,j)的區域。因此如果我們保證 (p, q) 在矩形[0, 0, i, j]之外(除了特殊情況在(i,j)處重疊),它將絕不會出現在(0, 0) ==> (i-1, j) or(0, 0) ==> (i, j-1)的路徑上。

同理 (i, j) 也必須落在矩形 [0, 0, p, q] 之外,避免重複計數。總結得到以下三個條件之一應該為true

  1. i < p && j > q
  2. i == p && j == q
  3. i > p && j < q

這說明往返路程T(i, j, p, q)並非對所有四個座標的取值都有效,而應該滿足上述條件。

但是 T(i, j, p, q) 不滿足self-consistency, For example, T(3, 1, 2, 3) is valid under these conditions but one of the terms in the recurrence relations, T(2, 1, 2, 2), would be invalid, and we have no idea how to get its value under current definition of T(i, j, p, q).

Self-consistent two-leg DP definition

因此上面四個引數是互相關聯的,不是獨立引數。上述四個引數的表示式不是最簡的,我們希望能夠找到滿足上述三個條件的子集,以確保一定滿足上述條件。我們可以觀察到**:當 i(p) 增加時,我們必須讓 j(q) 減小使得上式滿足,反之亦然**(它們是負關聯的)。於是我們可以設定i (p) 和j (q) 的和為某個常數,n = i + j = p + q(Note in this subset of conditions, n can be interpreted as the number of steps from the source position (0, 0). I have also tried other anti-correlated functions for i and j such as their product is a constant but it did not work out. The recurrence relations here play a role and constant sum turns out to be the simplest one that works.)

由此新條件,我們可以重寫滿足 n = i + j = p + q的式子T(i, j, p, q)為:

T(n, i, p), where T(n, i, p) = T(i, n-i, p, n-p).

T(i-1, n-i, p-1, n-p) = T(n-1, i-1, p-1)
T(i-1, n-i, p, n-p-1) = T(n-1, i-1, p)
T(i, n-i-1, p-1, n-p) = T(n-1, i, p-1)
T(i, n-i-1, p, n-p-1) = T(n-1, i, p)

於是得到遞迴表示式為:

T(n, i, p) = grid[i][n-i] + grid[p][n-p] + max{T(n-1, i-1, p-1), T(n-1, i-1, p), T(n-1, i, p-1), T(n-1, i, p)}.

Of course, in the recurrence relation above, only one of grid[i][n-i] and grid[p][n-p] will be taken if i == p (i.e., when the two positions overlap). Also note that all four indices, i, j, p and q, are in the range [0, N), meaning n will be in the range [0, 2N-1) (remember it is the sum of i and j). Lastly we have the base case given by T(0, 0, 0) = grid[0][0].

上述式子,時間複雜度:O(n3),空間複雜度:O(n3);但是觀察到T(n, i, p) only depends on those subproblems with n - 1,所以空間複雜度可以降為O(n^2)。

程式碼

/*
 * 動態規劃
 * 時間複雜度O(n^3) 空間複雜度O(n^2)
 */

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int N = grid.size(), M = (N<< 1) -1;    // M表示從(0,0)到(N-1,N-1)的格子數
        vector<vector<int>> dp(N, vector<int> (N,0));
        dp[0][0] = grid[0][0];

        for (int n = 1; n < M; ++n) {
            for (int i = N-1; i >= 0; --i) {
                for (int p = N-1; p >=0 ; --p) {
                    int j = n - i, q = n-p;
                    // 出界判斷,出現障礙
                    if (j < 0 || j >= N || q < 0 || q >= N || grid[i][j] < 0 || grid[p][q] < 0) {
                        dp[i][p] = -1;
                        continue;
                    }

                    if(i>0) dp[i][p] = max(dp[i][p], dp[i-1][p]);
                    if (p > 0) dp[i][p] = max(dp[i][p], dp[i][p - 1]);
                    if (i > 0 && p > 0) dp[i][p] = max(dp[i][p], dp[i - 1][p - 1]);

                    if (dp[i][p] >= 0) dp[i][p] += grid[i][j] + (i != p ? grid[p][q] : 0);
                }
            }
        }

        return max(dp[N-1][N-1],0);
    }
};

相關文章