62. 不同路径

前言

「力扣题解」系列致力于分享有价值的题目、探讨更优秀的解法。

这是本系列的第 2 篇题解,更多题解关注 Somnia1337@力扣。

从本篇起,新增 C++ 题解,由 @陈冥雁 提供。

基础知识

动态规划(Dynamic Programming):将原问题分解为一系列子问题,用“小”子问题的解递推“大”子问题的解,直到解出原问题。

“programming”指的是一种表格法,并非编写计算机程序。

——《算法导论》

为了避免重复计算,通常用数组记录子问题的解,该数组习惯命名为 dp

用动态规划解题可分为 5 步:

  1. 确定 dp 及下标含义。
  2. 确定子问题解的递推公式。
  3. 确定 dp 如何初始化。
  4. 确定遍历顺序。
  5. 举例推导。

题目描述

难度:🟡中等

标签:#数学动态规划组合数学

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

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

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

输入:m = 3, n = 7
输出:28

题意理解

输入:int * 2,网格的长、宽。

输出:int,不同的路径数。

给定网格的长、宽,只能向下或向右移动,计算从左上角移动到右下角的不同路径总数。

举例:对长 2、宽 3 的网格,简单画图可知,共有 3 条不同的路径。

解题思路

另一种方法:组合数学

本题可以用组合数解决,在 Java 等语言中需要处理溢出问题:

而在 Python 中可以直接调用库函数 comb,一行解决本题:

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        return comb(m + n - 2, n - 1)

但是,一些更复杂的相关问题难以用纯数学解决(见“相关题目”)。

本题是理解用动态规划解决路径问题的绝佳选择,因此本篇重点在于介绍动态规划,而不只是解决本题。

第 1 步:确定 dp 及下标含义

定义子问题。

子问题是,取终点之前的一个位置,计算从起点到该位置的不同路径数,例如:

共有 m * n 个子问题,要记录它们的解,因此 dp 开到 int[m][n]

int[][] dp = new int[m][n]; // 创建dp数组

dp[i][j] 表示从 的不同路径数。

第 2 步:确定子问题解的递推公式

找到子问题解之间的关系。

任取一个位置

到达 的上一个可能位置有哪些?

题目规定路径只能向下或向右,因此上一个位置只可能为上侧或左侧:

所以,dp[i][j] 来源于 dp[i - 1][j]dp[i][j - 1]

从这两个位置到达 dp[i][j] 都只有 1 条路径:

因此,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],这就是子问题解的递推公式。

原问题是所有子问题中最大的那个,最终答案为 dp[m - 1][n - 1]

return dp[m - 1][n - 1]; // 最大子问题即为原问题
第 3 步:确定 dp 如何初始化

初始化:最小子问题、递推公式无法涵盖的子问题。

考虑递推关系,dp[i][j] 从其上侧和左侧推导而来,可知第 0 行、第 0 列没有被涵盖(分别没有上一行、左一列),需要初始化。

初始化为几呢?

只有 1 条路径,因此第 0 行、第 0 列均填入 1。

for (int i = 0; i < m; i++) dp[i][0] = 1; // 初始化第0列
for (int j = 0; j < n; j++) dp[0][j] = 1; // 初始化第0行

第 4 步:确定遍历顺序

确保当前问题依赖的子问题已解出。

考虑递推关系,从左到右一层一层遍历,就能保证在计算 dp[i][j] 前,dp[i - 1][j]dp[i][j - 1] 都已解出。

for (int i = 1; i < m; i++) { // 从1开始
	for (int j = 1; j < n; j++) { // 从1开始
		dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 递推公式
	}
}
第 5 步:举例推导

检查思路的正确性。

m = 3, n = 7 为例推导:

答案为 28,符合预期,思路没有问题,就差代码实现啦!

复杂度

  • 时间复杂度:
  • 空间复杂度:

代码

代码不长,建议结合讲解仔细阅读,重新理解一下。

Java
class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n]; // 创建dp数组
        for (int i = 0; i < m; i++) dp[i][0] = 1; // 初始化第0列
        for (int j = 0; j < n; j++) dp[0][j] = 1; // 初始化第0行
        for (int i = 1; i < m; i++) { // 从1开始
            for (int j = 1; j < n; j++) { // 从1开始
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 递推公式
            }
        }
        return dp[m - 1][n - 1]; // 最大子问题即为原问题
    }
}
Python
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] * n for _ in range(m)] # 创建dp数组
        for i in range(0, m): # 初始化第0列
            dp[i][0] = 1
        for j in range(0, n): # 初始化第0行
            dp[0][j] = 1
        for i in range(1, m): # 从1开始
            for j in range(1, n): # 从1开始
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1] # 递推公式
        return dp[-1][-1] # 最大子问题即为原问题
C++

由 @陈冥雁 提供。

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n)); // 创建dp数组
        for (int i = 0; i < m; i++) { // 初始化第0列
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) { // 初始化第0行
            dp[0][j] = 1;
        }
        for (int i = 1; i < m; i++) { // 从1开始
            for (int j = 1; j < n; j++) { // 从1开始
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 递推公式
            }
        }
        return dp[m - 1][n - 1]; // 最大子问题即为原问题
    }
};

相关题目

以下是 2 道相似的路径问题,可用动态规划解决。

63. 不同路径 II

数组动态规划矩阵

64. 最小路径和

数组动态规划矩阵

更多

还有一份我完成过的题单,其中标出了主观的“价值”评分,供参考:

最后

更多系列「外源推文」&「生活分享」关注公众号: