动态规划第四篇:不同路径 + 不同路径(有障碍物) + 整数拆分 + 不同的二叉搜索树

130 阅读3分钟

文章目录

62.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 :
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

向右 -> 向右 -> 向下
向右 -> 向下 -> 向右
向下 -> 向右 -> 向右

机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
  2. 确定递推公式
    想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。

此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。

那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。

  1. dp数组的初始化
    如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。

所以初始化代码为:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

  1. 确定遍历顺序
    这里要看一下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。

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];
    }
}

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

图片
网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:

图片
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

向右 -> 向右 -> 向下 -> 向下
向下 -> 向下 -> 向右 -> 向右
示例 2:

图片
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1

动态规划代码:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m=obstacleGrid.length;
        int n=obstacleGrid[0].length;
        int[][] dp=new int[m][n];  // 初始化是每个数据都是0
        for (int i=0;i<m;i++){
            if (obstacleGrid[i][0]==0)  // 0表示没有障碍   如果障碍物为1,表示此处无法达到, 就是默认的0
                dp[i][0]=1;
            else break;   // 因为边界位置只有一条路,所以后面的也无法达到,后面的即使不是障碍物,也要赋值为0
        }
        for (int j=0;j<n;j++){
            if (obstacleGrid[0][j]==0)
                dp[0][j]=1;
            else break;  // 因为边界位置只有一条路,所以后面的也无法达到,后面的即使不是障碍物,也要赋值为0
        }
        for (int i=1;i<m;i++){
            for (int j=1;j<n;j++){
                if (obstacleGrid[i][j]==0)    // 如果障碍物为1,此处无法达到,就是默认的0
                    dp[i][j]=dp[i][j-1]+dp[i-1][j];   
                    // 中间节点有两条路,所以不会直接else break;
            }
        }
        return dp[m-1][n-1];
    }
}

增加障碍物之后,对于上边界、左边界、内部的处理情况是不一样的

343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。

示例 1: 输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。

思考:拆的个数没有指定,拆分成两个呢,还是三个呢,还是四个…

class Solution {
    public int integerBreak(int n) {  // n [2,58]
        //     if (n==2) return 1;  没有必要  后面return dp[n] 当 n==2的时候,可以返回dp[2]
        int[] dp = new int[n + 1];  // 0和1没有, 从 [2,n]  因为 n [2,58] 所以 dp [2,58]
        dp[2] = 1;  // dp[0] dp[1] 没有关系
        for (int i = 3; i <= n; i++) {
            for (int j = 1; j < i ; j++)
                dp[i] = Math.max(dp[i], Math.max((i - j) * j, dp[i - j] * j));
        }
        return dp[n]; // n给定2,返回为dp[2] ,n给定其他值,返回dp[n]
    }
}

注意1:特殊值处理没有必要, if (n==2) return 1; 没有必要 后面return dp[n] 当 n==2的时候,可以返回dp[2]

注意2:数组必须定义时指定长度,这里定义 dp[i]的含义是分拆数字i,可以得到的最大乘积为dp[i]。所以,最后返回的应该是 dp[n] ,所以应该定义长度为 new int[n+1] 。

注意3:for (int i = 3; i <= n; i++)
(1)因为dp[0] dp[1] 没有意义,dp[2]=1 作为了初始值,所以 i 从3开始
(2)因为最后要返回的是 dp[n] 所以循环条件是 i<=n 而不是 i<n
(3)因为 dp 数组后面的要由前面的推出,所以这里是 i++

注意4:for (int j = 1; j < i - 1; j++)
(1)因为对当前数字 i 进行拆分,所以要从 i = 1 +(i-1)开始,所以 j 从1开始,当 j=1 时,i-j = i-1;
(2)因为对当前数字 i 进行拆分,所以要从 i = 1 +(i-1)开始,所以 j 到 i-1 为止,当j=i-1时,i-j=1;为什么也可以写成 j 到 i-2 为止?因为
(3)j 也是从小到大,所以 j++。

注意5:dp[i] = Math.max(dp[i], Math.max((i - j) * j, dp[i - j] * j));
(1)Math.max((i - j) * j, dp[i - j] * j) (i-j)* j 表示前面 (i-j) 没有被拆分,i 仅仅被拆了一次,所以结果是(i-j)* j 。 dp[i - j] * j 表示前面 dp[i - j] 有被拆分,dp数组的下标为 (i-j) 的位置记录的就是拆分的乘积最大值,现在又拆一个 j 出来,所以结果是 dp[i - j] * j 。最后用 Math.max 取出 i 仅仅拆一次和拆多次的最大值。
(2)Math.max(dp[i], Math.max((i - j) * j, dp[i - j] * j));
因为 j++ ,所以每次都会更新 dp[i] ,每次 j++ 就得到一个新的 dp[i] ,新的dp[i] 和之前旧的最大的dp[i]比较,较大的存放到 dp[i] 中。

96.不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:在这里插入图片描述

class Solution {
    public int numTrees(int n) {  // n  [1,19]
        int[] dp=new int[n+1];
        dp[0]=1; // 没什么 n不会为0,这是用来乘法的
        for (int i=1;i<=n;i++){
            for (int j=1;j<=i;j++)
                dp[i]=dp[i]+dp[i-j]*dp[j-1];
        }
        return dp[n];
    }
}

逐行解释:

注意1:dp[0]=1; // 没什么 n不会为0,这是用来乘法的

注意2:for (int i=1;i<=n;i++)
(1)dp[i] 的含义是:1到i为节点组成的二叉搜索树的个数为dp[i]。因为dp[0]已经赋值了,所以循环里面计算的从 dp[1] 开始,i 从1开始。
(2)因为 给定输入是n ,最后要返回1到n为节点组成的二叉搜索树的个数dp[n],所以 i 一定要在 n 为止,不能仅仅到 n-1 。
(3)从小到大,后面的依赖前面的,所以 i++ 。

注意3:for (int j=1;j<=i;j++)
(1)j 从 1 开始是因为 dp[i-1] * dp[0] ;
(2)j 到 i 为止是到最后一个 dp[0] * dp[i-1] 。
两端都是从下标0开始,到下标0为止,j 的循环是和 dp[i]=dp[i]+dp[i-j]*dp[j-1] 这一句联系在一起的。

注意4: dp[i]=dp[i]+dp[i-j]*dp[j-1];
(1)dp[i-j]*dp[j-1] 因为 j 从1开始,到 i 为止,所以是 dp[i-1]*dp[0] + dp[i-2]*dp[1] + … + dp[0]*dp[i-1]
(2)dp[i]=dp[i]+dp[i-j]*dp[j-1]; 就是一个累加而已。

目的是要达到 dp[i] = dp[i-1]*dp[0] + dp[i-2]*dp[1] + … + dp[0]*dp[i-1]
j 的循环 for (int j=1;j<=i;j++)dp[i]=dp[i]+dp[i-j]*dp[j-1]; 都是为了这个目的。