Day 34 动态规划 Part02

动态规划解题步骤

  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];
    }
}
posted @   12点不睡觉还想干啥?  阅读(19)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示