Day 34 動態規劃 Part02

12点不睡觉还想干啥?發表於2024-08-05

動態規劃解題步驟

  1. 確定dp陣列(dp table)以及下標的含義
  2. 確定遞推公式
  3. dp陣列如何初始化
  4. 確定遍歷順序
  5. 舉例推導dp陣列

[62. 不同路徑](https://leetcode.cn/problems/fibonacci-number/)

動態規劃的解法還是很好理解的。按照解題步驟來。

  1. 確定dp陣列及其下標的含義。dp[i][j]代表到達[i, j]位置處的路徑數
  2. 確定遞推公式。這裡最重要的點就是[i,j]點只能從其上或右側的點到達。因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
  3. dp初值的確定。顯然,第一行和第一列的所有節點都只有1種到達方式。
  4. 按從上到下從左到右去遍歷即可。
class Solution {
    public int uniquePaths(int m, int n) { 
        int[][] dp = new int[m][n];
        for(int i = 0; i < m; i++) dp[i][0] = 1;
        for(int i = 0; i < n; i++) dp[0][i] = 1;

        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

class Solution { //滾動陣列最佳化時間複雜度
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[j] += dp[j-1];
            }
        }
        return dp[n-1];
    }
}

這道題還有數學的解法,其實我最開始也是這麼想的,但可能是排列組合忘太久了,想到的思路並不正確。只能看到題解後,哦哦哦,原來是這樣。其實就是組合數,從起點走到終點一共有m+n-2步,其中一定有 m-1 步向下走(等價於一定有 n-1 步向下走),因此就是組合數$C_{m+n-2}^{m-1}$。這裡需要注意的就是可能出現越界的問題,所以在計算組合數時需要邊乘邊除。貼上程式碼,自己理解吧。

class Solution {
    public int uniquePaths(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
            ans = ans * x / y;
        }
        return (int) ans;
    }
}

63. 不同路徑 II

這道題就不能使用數學的方式了。只能使用 dp 的方法來做。基本上與上一題是一致的,但是需要處理遇到石頭時的情況。這道題還可以使用 滾動陣列 最佳化,第一種方式熟練之後,很容易寫出第二種。

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
    
        int[][] dp = new int[m][n];
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;

        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

class Solution { //滾動陣列最佳化空間複雜度
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[n];
        for(int j = 0; j < n && obstacleGrid[0][j] != 1; j++) dp[j] = 1;
        for(int i = 1; i < m; i++)
            for(int j = 0; j < n; j++)
                if(obstacleGrid[i][j] == 1) dp[j] = 0;
                else if(j != 0) dp[j] += dp[j-1];
        return dp[n-1];
    }
}

343. 整數拆分

這道題的思路真的太牛逼,照著這個思路誰都能寫出來,但這個思路想不到就是寄。對於一個數字,要麼分成兩個數字的乘積,要麼分成多個數字的乘積,最大值一定是這兩種情況之一,因此考慮用動態規劃。但是怎麼表示多個數字的乘積呢。這裡我就直接搬來題解中的 dp 陣列的定義了。

  1. dp陣列的定義:dp[i]代表數字 i 使得乘積最大的劃分方式得到的乘積。
  2. 遞推公式:對於一個數字 n,顯然他能拆分成 (1 * n-1), (2 * n-2), (3 * n-3) ...,這是對應於拆分成兩個數字的乘積的部分,怎麼表示其拆分成多個數字的乘積呢。這裡就是最重要的部分了,數字n的拆分成多個數字乘積可以這樣表示 (1 * dp[n-1]), (2 * dp[n-2]), (3, dp[n-3])...
class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n+1];
        dp[2] = 1;
        for(int i = 3; i <= n; i++)
            for(int j = 1; j <= i/2; j++)
                dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j])); //別忘了還要和dp[i]自己比較,留存最優結果
        return dp[n];
    }
}

96. 不同的二叉搜尋樹

和上一題的思路基本一致,只有在遞推關係式部分有所不同。

  1. dp陣列的含義: dp[i]代表了i個節點的二叉搜尋樹的個數。
  2. 遞推公式:對於包含了n個節點的搜尋樹,顯然,根節點一定是必不可少的,其餘節點分配在兩個子樹上。以左子樹為準,可以包含 0 ~ n-1個節點,對應的右子樹就包含了 n-1 ~ 0 個節點,所以dp[左子樹節點個數] * dp[右子樹節點個數]的累加和就是總的數量。
class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1; dp[1] = 1;
        for(int i = 2; i <= n; i++){
            for(int j = 0; j < i; j++){ //j代表左子樹上節點個數
                dp[i] += dp[j] * dp[i-1-j];
            }
        }
        return dp[n];
    }
}

相關文章