动态规划习题篇(不同路径和整数拆分)

1.不同路径

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

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

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

示例 1:

复制代码
输入:m = 3, n = 7
输出:28

示例 2:

复制代码
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

复制代码
输入:m = 7, n = 3
输出:28

示例 4:

复制代码
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109
cs 复制代码
int uniquePaths(int m, int n) {
    int dp[m][n];
    
    for (int i = 0; i < m; i++) dp[i][0] = 1;
    for (int j = 0; j < n; j++) dp[0][j] = 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];
}

根据题目描述,了解机器人从(0,0)位置出发,到(m-1,n-1)终点。

根据动态规划五部曲来分析:

1.确定dp数组的含义及下标含义

因为相当于是坐标问题,所以是二维数组动态规划。

dp[i][j]表示从(0,0)出发到(i,j)有dp[i][j]条不同路径。

2.确定递推公式

根据上图以及题意,发现只有两个方向来推导出来,即向下和向右。所以到达这一位置坐标的上一个状态是dp[i - 1][j] 和 dp[i][j - 1]。dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。

可能有人觉得应该是dp[i-1][j]+1才是到达当前状态的正确式子,但这就是dp[i][j]含义没有搞懂,它表示的是路径数,不是步数,在前一个状态到当前状态路径没有变化,就是原来的路径数。

3.dp数组的初始化(有点难想,但特别重要)

因为要从上到下,从左到右,所以第一行和最左边第一列都应该初始化,这样才能用前面的状态来求后面的状态,否则不初始化都是垃圾值。

因为只能向下和向右,所以第一行和第一列的路径数都是1.

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

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

4.确定遍历顺序

根据递推公式,都是从上方和左方一路推导来的,所以从左往右,从上往下进行遍历。

5.打印dp数组

2.不同路径II

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角 (即 grid[0][0])。机器人尝试移动到 右下角 (即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 10 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 * 109

示例 1:

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

示例 2:

复制代码
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01
cs 复制代码
int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize) {
    int m = obstacleGridSize;
    int n = obstacleGridColSize[0];
    
    if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {
        return 0;
    }
    
    int dp[m][n];
    
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            dp[i][j] = 0;
        }
    }
    
    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];
}

上述代码无需过度纠结前面的m,n表示,主要是理解动态规划思路。

动态规划五部曲:

1.确定dp数组含义及下标含义

dp[i][j]还是表示从起点到(i,j)的不同路径数

2.确定递推公式
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
但这里要注意,当没有障碍物的时候才能够继续进行。
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}

3.dp数组初始化(在本题中,最需要注意的就是初始化发生巨大变化)

之前没有障碍时,第一行直接初始化为1。但只要第一行有障碍,经分析,后面的就都到不了了,所以障碍后面的路径数是0.前面的路径数是1.第一列和第一行一样,都是按上述进行初始化。

4.确定遍历顺序

5.打印dp数组

为什么在有障碍物的题目中需要进行全数组初始化,而在没有障碍物的题目中可以指初始化边界?

无障碍物题目:

  • 所有位置都一定会被计算到

  • (1,1) 开始,每个位置都通过公式计算

  • 不会使用未初始化的值

有障碍物题目:

內部是否初始化为0呢?

for (int i = 0; i < m; i++) {

for (int j = 0; j < n; j++) {

dp[i][j] = 0;

}

}

我一开始觉得不用初始化,因为都会通过递推公式求解到值。但是我忽略了,如果这个位置正好是障碍物,那就会执行跳过障碍物的语句,即continue,跳过递推公式后就没办法通过前面来计算当前位置的路径数,所以在计算后面位置的路径数的时候,就会以它的初始值进行计算,而如果不初始化,那它的值就是垃圾值,所以得到的也是垃圾值。

对于障碍物位置 (1,1)

  • if (obstacleGrid[1][1] == 1) continue;

  • 直接跳过,不会执行赋值语句

  • dp[1][1] 保持未初始化状态(随机值)

3.整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积
示例 1:

复制代码
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

复制代码
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58
cs 复制代码
int integerBreak(int n) {
    int dp[n + 1];
    
    // 初始化dp数组为0
    for (int i = 0; i <= n; i++) {
        dp[i] = 0;
    }
    
    dp[2] = 1;
    
    for (int i = 3; i <= n; i++) {
        for (int j = 1; j <= i / 2; j++) {
            int option1 = (i - j) * j;
            int option2 = dp[i - j] * j;
            int current = option1 > option2 ? option1 : option2;
            if (current > dp[i]) {
                dp[i] = current;
            }
        }
    }
    
    return dp[n];
}

解题思路:
分析发现,将某个数拆成m个数,这m个数近似相等,相乘才是最大的。
动态规划五部曲:
1.dp[i]表示拆分数字i,可以得到最大的乘积为dp[i]。
2.确定递推公式
考虑一下怎么才能使dp[i]出现包含两个三个或更多的整数乘积进行比较呢?
可以从1遍历j,然后有两种渠道得到dp[i],一种是j * (i - j) 直接相乘,即拆分成两个整数相乘;⼀个是j * dp[i - j],相当于是拆分(i - j),不断拆分,可以分成很多个整数的乘积。
那为什么j可以不用拆分呢?
其实拆分i-j就已经把拆分j的情况包含了。
举例6:
1*5//这里dp[1]不可以拆分,初始化就是1;5可以进行拆分
2*3//这里2是遍历的固定的j不可以拆分,3可以拆分;但是如果让2拆分的话,只能拆分成1*1,但是这个情况在上面1*5中是可以包含到的。
3*3//同理将3拆开,包含2的可以在2*3中得到,包含1的可以在1*1中得到
4*2
5*1
所以可以遍历j不变,不需要是dp[j]。
3.dp数组初始化
dp[0] dp[1] 就不应该初始化,也就是没有意义的数值,无法知道拆分后的乘积是多大,但初始化为0也可以,因为可以将乘dp[0]的值变成0,反正乘dp[0]的值也不知道拆分后的是多少。因为二者无法拆分。
4.遍历顺序
先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
dp[i] 是依靠 dp[i - j]的状态,所以遍历i⼀定是从前向后遍历,先有dp[i - j]再有dp[i]。而且是先遍历i,再遍历j。
j遍历到i/2,是因为根据拆分数字发现,当拆分成两个整数的时候,两个整数相等,则乘积更大。当拆分成3个及以上整数的时候,发现m个数越相近,乘积越大。拆分成m个数,这些数肯定小于这个数i/2,而拆分成两个数的时候,这两个数刚好等于i/2,所以j只需要遍历到i/2就足够。

相关推荐
难得的我们6 小时前
基于C++的区块链实现
开发语言·c++·算法
季明洵6 小时前
Java实现顺序表
java·数据结构·算法·顺序表
Σίσυφος19006 小时前
点云计算曲率以及法向量的方向问题
算法
大闲在人6 小时前
使用有向莱顿算法进行供应链/物料流转网络的集群划分
网络·算法
im_AMBER6 小时前
Leetcode 110 奇偶链表
数据结构·学习·算法·leetcode
踩坑记录8 小时前
leetcode hot100 easy 101. 对称二叉树 递归 层序遍历 bfs
算法·leetcode·宽度优先
2501_940315269 小时前
leetcode182动态口令(将字符的前几个元素放在字符串后面)
算法
老鼠只爱大米9 小时前
LeetCode经典算法面试题 #98:验证二叉搜索树(递归法、迭代法等五种实现方案详解)
算法·leetcode·二叉树·递归·二叉搜索树·迭代
疯狂的喵14 小时前
C++编译期多态实现
开发语言·c++·算法