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])。机器人每次只能向下或者向右移动一步。
网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。
返回机器人能够到达右下角的不同路径数量。
测试用例保证答案小于等于 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.lengthn == obstacleGrid[i].length1 <= m, n <= 100obstacleGrid[i][j]为0或1
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就足够。