62. 不同路径
题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
思路
网上有很多算法,常见的可能就两种
一种是数学公式(排列组合),如下:
- 从左上角到右下角的过程中,一共需要移动 m+n-2 次
- m-1次向下移动
- n-1次向右移动
- 路径的总数,等于从 m+n-2 次移动中选择 m-1 次向下移动的方案
另外一种便是使用动态规划解决,思路如下:
- 首先,定义状态dp[i][j]表示,到达 i, j 最多路径
- 构建状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
到达 i,j 相当于计算:到达上一步的位置的两种情况路径总和
后退一步包含两种情况(通过题目意思可知):向左后退一步(i-1),向上后退一步(j-1)
从坐标 (0,0) 到坐标 (i,j) 的路径总数的值只取决于 从坐标 (0,0) 到坐标 (i−1,j) 的路径总数 和 从坐标 (0,0) 到坐标 (i,j−1) 的路径总数,即 f(i,j) 只能通过 f(i−1,j) 和 f(i,j−1) 转移得到 - 构建初始边界条件:
对于第一行 dp[0][j],或者第一列 dp[i][0],由于都是在边界,所以只能为 1
看题,要求如下,所以只能是1
- 机器人每次只能向下或者向右移动一步
- 求总共有多少条不同的路径
- 最后,确定计算顺序,自底向上(从最小子问题逐步推导,最终得到全局解)
从1这个下标开始依次计算,最后返回要求值dp[m-1][n-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;
}
}
动态规划
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 的整数数组 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 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
思路
还是使用动态规划
- 首先,定义状态dp[i][j]表示,到达 i, j 最多路径
- 其次,构建状态转移方程,有两种情况:
- 不是障碍物:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
和上面的62一样的原因
- 是障碍物(当前坐标不可达):0
- 空间优化:f(i,j) 只与 f(i−1,j) 和 f(i,j−1) 相关,所以可以使用滑动数组的概念,我们只需要使用一维数组保存数量即可
要求值,因为我们每次只需要 dp[i-1][j],dp[i][j-1],所以我们只要记录这两个数,而因为边界只是会在第一次运算时,被使用到
比如:在计算dp[1][1] 时,会用到 dp[0][1]
,但是之后就不再用到了。所以可以把 dp[1][1] 记到 dp[0][1]
中,这样对于 dp[1][2](dp[0][2]+dp[1][1]) 来说,它需要的数据就在 dp[0][1] 和 dp[0][2] 中。dp[1][2] 算完后也可以同样记到 dp[0][2]
中。dp[2][2]
=dp[1][2]+dp[2][1]=dp[0][2]+dp[1][1]+dp[2][0]=dp[0][2]+dp[0][1]+dp[2][0]
解释:因为是到末尾,所以变为一维数组后- dp[j] 在更新前存储的是 dp[i-1][j](
上一行
的值)。 - dp[j-1] 存储的是 dp[i][j-1](当前行已计算的值)。
- 更新 dp[j] 时,相当于 dp[i][j] = dp[i-1][j] + dp[i][j-1]。
- dp[j] 在更新前存储的是 dp[i-1][j](
- 不是障碍物:
- 之后,构建初始边界条件:第一行和第一列值的计算,不是障碍物坐标值就为1,如果有障碍物,后面的是不可达的
- 最后,选择自底向上迭代获取每一个坐标
这个方法,可以直接使用上面没有障碍物的情况的步骤一致,只需要有两点需要补充
- 初始化条件需要多一个:dp[0][0] 这个开始点就是障碍
- 在进行计算dp的时候,还需要去判断当前坐标值必须为0才可以
代码
使用二维数组存储
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int xc=obstacleGrid[0].length, yr=obstacleGrid.length;
int[][] dp = new int[yr][xc];
for(int i=0;i<xc;++i){
if(obstacleGrid[0][i]==1) break;
dp[0][i]=1;
}
for(int i=0;i<yr;++i){
if(obstacleGrid[i][0]==1) break;
dp[i][0]=1;
}
dp[0][0]=obstacleGrid[0][0]==1?0 : 1;
for(int i=1;i<yr;++i){
for(int j=1;j<xc;++j){
if(obstacleGrid[i][j]==0){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[yr-1][xc-1];
}
}
优化,使用一维数组存储
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int xc = obstacleGrid[0].length, yr = obstacleGrid.length;
int[] dp = new int[xc];
dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1;
for (int i = 0; i < yr; ++i) {
for (int j = 0; j < xc; ++j) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0;
continue;
}
if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
dp[j] += dp[j - 1];
}
}
}
return dp[xc - 1];
}
}
63. 不同路径 III(回溯)
题目描述
在二维网格 grid 上,有 4 种类型的方格:
1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。
每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
示例 1:
输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
输出:2
解释:我们有以下两条路径:
- (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
- (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)
示例 2:
输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
输出:4
解释:我们有以下四条路径:
- (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
- (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
- (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
- (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)
示例 3:
输入:[[0,1],[2,0]]
输出:0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。
提示:1 <= grid.length * grid[0].length <= 20
思路
首先,选择路径
:定义函数 dfs,表示当前 grid 状态下,从点 (x,y) 出发,还要经过 n 个点,走到终点的路径条数
每一个无障碍方格都要通过一次,所以必须要经过所有的点
其次,递归探索
:
- 如果当前的点不为终点,则将当前的点标记为 −1,表示这条路径以后不能再经过这个点,
- 然后继续在这个点往四个方向扩展,
- 如果不超过边界且下一个点的值为 0 或者 2,则表示这条路径可以继续扩展
然后,确定终止条件:
- 到达一个点时,如果当前的点为终点,且已经经过了 (n+1) 个点,那么就构成了一条合格的路径,否则就不构成
如果 grid[x][y]=2,说明到达终点。如果此时 n=0 则返回 1 表示找到了一条合法路径;否则返回 0 表示不合法的路径。
- 如果当前的点不为终点,则将当前的点标记为 −1,表示这条路径以后不能再经过这个点,
如果出界,或者 grid[x][y]=−1,则返回 0,表示不合法的路径。
最后,回退与恢复:探测完四个方向后,需要将当前的点的值改为原来的值
代码实现时,可以把 grid[x][y] 改成 −1,表示这个格子访问过,在返回前改回 0(恢复现场)。
代码
class Solution {
public int uniquePathsIII(int[][] grid) {
int n = 0, x = -1, y = -1;
for (int i = 0; i < grid.length; ++i) {
for (int j = 0; j < grid[i].length; ++j) {
if (grid[i][j] == 0) {
// 每一个无障碍方格都要通过一次,计算所有无障碍方格
++n;
}
if (grid[i][j] == 1) {
// 计算起始点
x = i;
y = j;
}
}
}
// n+1 算上起点,也是必须要经过的点
return dfs(grid, x, y, n + 1);
}
private int dfs(int[][] grid, int x, int y, int n) {
if (x < 0 || x >= grid.length || y < 0 || y >= grid[x].length || grid[x][y] < 0) {
// 不合法数据
return 0;
}
if (grid[x][y] == 2) {
// 到终点,需要判断是否所有结点都被经过,有没有经过的返回0
return n == 0 ? 1 : 0;
}
// 将当前结点设置为已被使用,后续路径便不能再使用此坐标
grid[x][y] = -1;
// 上、下、左、右移动 并都 n-1,计算所有和
int ans = dfs(grid, x - 1, y, n - 1) + dfs(grid, x, y - 1, n - 1) +
dfs(grid, x + 1, y, n - 1) + dfs(grid, x, y + 1, n - 1);
// 恢复(回溯)
grid[x][y] = 0;
return ans;
}
}