一、动态规划三大步骤
之前在微信公众号【帅地玩编程】上看到有人分享,解决动态规划问题的三大步骤,今天使用这个解题套路,解决LeetCode中不同路径问题。
这个问题,LeetCode中共有三题,难度从由低到高,分别为【62题 不同路径】、【63题 不同路径2】、【980题 不同路径3】。
先上解题套路(下面是作者原话):
动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤,
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
学过动态规划的可能都经常听到最优子结构,把大的问题拆分成小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计你们也听多了,所以这一次,我将换一种形式来讲,不再是各种子问题,各种最优子结构。所以大佬可别喷我再乱讲,因为我说了,这是我自己平时做题的套路。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。
不太理解,下面将其应用到题目中就能明白。
二、不同路径
题目(难度:中等):
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
步骤一、定义数组元素的含义
由于我们的目的是从左上角到右下角一共有多少种路径,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。那么,dp[m-1] [n-1] 就是我们要的答案了(网格为m*n)。
步骤二:找出关系数组元素间的关系式
想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走
上代码
public static 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 j=0; j<n; j++){
dp[0][j] = 1;
}
// 通过关系式推导出dp[m-1][n-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];
}
三、不同路径2
题目(难度:中等):
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
步骤一、定义数组元素的含义
数组含义仍然跟上一题一样,当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。仍然是求dp[m-1] [n-1] 。
步骤二:找出关系数组元素间的关系式
跟上一题相同,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
因为有障碍了,如果(i,j)这个位置是障碍,那么不可能到达(i,j)这个位置,那么dp[i] [j] = 0。
步骤三、找出初始值
我们的初始值还是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。相当于计算机图中的最上面一行和左边一列。
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走
但是跟上一题有个不同的地方,如果最上面一行或者最左边一列存在障碍,假设(0,j)存在障碍,dp[0][j] = 0,很好理解,因为肯定无法到达这个位置;值得注意,dp[0][j+1....n-1] = 0,因为(0,j)存在障碍,那就不可能一直往左走了,所以(0,j)右边都是0。
上代码
public static int UniquePathsWithObstacles(int[][] obstacleGrid) {
int m= obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
if(obstacleGrid[0][0] == 1){
return 0;
}else {
dp[0][0] = 1;
}
// 初始化,如果第一列存在障碍,第一列后面都是0
for(int i=1; i<m; i++){
if(obstacleGrid[i][0] == 1){
dp[i][0] = 0;
}else{
dp[i][0] = dp[i-1][0];
}
}
// 初始化,如果第一行存在障碍,第一行后面都是0
for(int j=1; j<n; j++){
if(obstacleGrid[0][j] == 1){
dp[0][j] = 0;
}else{
dp[0][j] = dp[0][j-1];
}
}
// 推导出dp[m-1][n-1]
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
if(obstacleGrid[i][j] == 1){
dp[i][j] = 0;
}else{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
四、不同路径3
题目(难度:困难):
在二维网格 grid 上,有 4 种类型的方格:
1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目,每一个无障碍方格都要通过一次。
本题不会用动态规划,实力不允许,找不到数组元素间的关系式,使用递归进行解答。
从起点1开始尝试遍历每一个 0 方格,并且将走过的方格放入一个map,用于标记走过了。当回溯时,必须将标记的方格从map中移除。
如果到达障碍点-1,则返回,此路不通。
如果到达终点2,并且经过所有无障碍方格(map的size等于所有除-1方格数),则表示map中是一条成功路径,成功路径+1
上代码
package dynamicProgram;
import java.util.LinkedHashMap;
import java.util.Map;
public class uniquePathsIII {
public static void main(String[] args){
int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
System.out.println("成功路径总数:" + uniquePathsIII(obstacleGrid));
}
public static int uniquePathsIII(int[][] grid) {
int m= grid.length;
int n = grid[0].length;
int starti=0;
int startj=0;
int step= 0;
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(grid[i][j] == 0 || grid[i][j] == 1 || grid[i][j] == 2){
step++;
}
if(grid[i][j] == 1){
starti = i;
startj = j;
}
}
}
Map<String, String> map = new LinkedHashMap<>();
map.put("(" + starti + ", " + startj + ")", "0");
int ret = 0;
if(starti + 1 < grid.length){
ret += access(starti+1, startj, grid, step, map);
}
if(starti - 1 >= 0){
ret += access(starti - 1, startj, grid, step, map);
}
if(startj + 1 < grid[0].length){
ret += access(starti, startj+1, grid, step, map);
}
if(startj - 1 >= 0){
ret += access(starti, startj-1, grid, step, map);
}
return ret;
}
public static int access(int x, int y, int[][] grid, int step, Map<String, String> map) {
// 标记当前方格走过
map.put("(" + x + ", " + y + ")", "0");
// 如果到达终点,并且走遍所有无障碍方格,则为一条成功路径
if (grid[x][y] == 2 && step == map.size()) {
// 打印当前成功路径
System.out.println("成功路径;");
for(Map.Entry<String, String> mapEntry : map.entrySet()){
System.out.print(mapEntry.getKey() + " - ");
}
System.out.println();
// 因为map相当于全局变量,一定移除走过的方格
map.remove("(" + x + ", " + y + ")");
return 1;
}
if (grid[x][y] == -1) {
// 因为map相当于全局变量,一定移除走过的方格
map.remove("(" + x + ", " + y + ")");
return 0;
}
int ret = 0;
// 上下左右四个方向,并且是没走过的方格,都走一遍
if (x + 1 < grid.length && (map.get("(" + (x + 1) + ", " + y + ")") == null)) {
ret += access(x + 1, y, grid, step, map);
}
if (y + 1 < grid[0].length && (map.get("(" + x + ", " + (y + 1) + ")") == null)) {
ret += access(x, y + 1, grid, step, map);
}
if (x - 1 >= 0 && (map.get("(" + (x - 1) + ", " + y + ")") == null)) {
ret += access(x - 1, y, grid, step, map);
}
if (y - 1 >= 0 && (map.get("(" + x + ", " + (y - 1) + ")") == null)) {
ret += access(x, y - 1, grid, step, map);
}
// 因为map相当于全局变量,一定移除走过的方格
map.remove("(" + x + ", " + y + ")");
return ret;
}
}
我打印了成功路径,看看结果
上面代码从写法上,比较容易理解,再上一版,从代码写法上简洁点的,逻辑相同。
上代码:
package dynamicProgram;
public class UniquePathsIIIConcise {
int ans;
int[][] grid;
int tr, tc;
// 表示四个方向,不用四个if语句判断了
int[] dr = new int[]{0, -1, 0, 1};
int[] dc = new int[]{1, 0, -1, 0};
int R, C;
public static void main(String[] args){
int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
UniquePathsIIIConcise uniquePathsIIIConcise = new UniquePathsIIIConcise();
System.out.println("成功路径总数:" + uniquePathsIIIConcise.uniquePathsIII(obstacleGrid));
}
public int uniquePathsIII(int[][] grid) {
this.grid = grid;
R = grid.length;
C = grid[0].length;
int todo = 0;
int sr = 0, sc = 0;
for (int r = 0; r < R; ++r)
for (int c = 0; c < C; ++c) {
if (grid[r][c] != -1) {
todo++;
}
if (grid[r][c] == 1) {
sr = r;
sc = c;
} else if (grid[r][c] == 2) {
tr = r;
tc = c;
}
}
ans = 0;
dfs(sr, sc, todo);
return ans;
}
public void dfs(int r, int c, int todo) {
todo--;
if (todo < 0) return;
// 到达终点,并且经过所有无障碍方格
if (r == tr && c == tc) {
if (todo == 0) ans++;
return;
}
grid[r][c] = 3;
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k];
int nc = c + dc[k];
if (0 <= nr && nr < R && 0 <= nc && nc < C) {
// 没经过,并且不是障碍方格-1
if (grid[nr][nc] % 2 == 0)
dfs(nr, nc, todo);
}
}
grid[r][c] = 0;
}
}