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

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数组的含义及下标含义

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

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

2.确定递推公式

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

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

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

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

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

for (int i = 0; i < m; i++) dpi0 = 1;

for (int j = 0; j < n; j++) dp0j = 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数组含义及下标含义

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

2.确定递推公式
dpij = dpi - 1j + dpij - 1
但这里要注意,当没有障碍物的时候才能够继续进行。
if (obstacleGridij == 0) { // 当(i, j)没有障碍的时候,再推导dpij
dpij = dpi - 1j + dpij - 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++) {

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

相关推荐
江屿风几秒前
C++图论基础拓扑排序算法流食般投喂
开发语言·c++·笔记·算法·排序算法
海棠AI实验室7 分钟前
AI 时代文献综述:从检索到成稿的 RAG 五步法
windows·算法·自动化·llm·rag
H178535090967 分钟前
SolidWorks_基于草图的实体特征14_扫描扭转与控制
前端·人工智能·算法·3d建模·solidworks
黄金龙PLUS10 分钟前
基于ARX结构的新型序列密码算法FlashLight
算法·网络安全·密码学·哈希算法·同态加密
洛水水14 分钟前
【力扣100题】77.搜索二维矩阵
算法·leetcode·矩阵
仙俊红25 分钟前
深入理解 ThreadLocal —— 从变量引用、强弱引用到 Spring Boot 实战
spring boot·python·算法
故渊at29 分钟前
第五板块:Android 系统服务与电源管理 | 第十八篇:Battery Service 与 电量统计(Fuel Gauge)算法
android·算法·battery·电源·电池·电源管理·电量统计
The_Ticker31 分钟前
港股量化实测:实时行情接口性能与数据质量深度解析
python·websocket·算法·金融
weisian15132 分钟前
基础篇--概念原理-25-大模型的剪枝是什么?怎么理解?——从原理到实战,一篇讲透
算法·机器学习·大模型·剪枝
fie888936 分钟前
基于有限体积法(FVM)的MATLAB流体力学求解程序
算法·matlab