引言 在动态规划系列的前四篇文章中,我们分别学习了爬楼梯问题 、最长公共子序列 、01背包问题 和最长递增子序列 。今天我们将探讨动态规划的另一个重要分支——路径计数问题 。
路径计数问题是动态规划中非常重要的一类问题,它不仅在算法面试中频繁出现,还与组合数学紧密相关。通过学习这类问题,我们能够深入理解动态规划在计数问题中的应用。
问题背景 路径计数问题的核心思想是:在一个给定的空间中,计算从起点到终点的不同路径数量 。这类问题通常具有以下特征:
方向限制 :只能向特定方向移动(如向右、向下)路径唯一性 :每条路径由一系列移动步骤组成计数目标 :求所有可能路径的数量 
经典例题一:不同路径 问题描述 LeetCode 62. 不同路径 
一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径?
1 2 3 4 5 6 7 8 9 10 11 示例 1: 输入:m = 3, n = 7 输出:28 示例 2: 输入:m = 3, n = 2 输出:3 解释:从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下 
解法一:动态规划 1. 状态定义 定义 dp[i][j] 表示:从起点 (0,0) 到达位置 (i,j) 的不同路径数量。
2. 状态转移方程 由于机器人只能向右或向下移动,到达位置 (i,j) 的路径只能来自:
从上方 (i-1,j) 向下移动 
从左方 (i,j-1) 向右移动 
 
因此状态转移方程为:
1 dp[i][j] = dp[i-1][j] + dp[i][j-1] 
3. 边界条件 
dp[0][j] = 1:第一行只有一条路径(一直向右)dp[i][0] = 1:第一列只有一条路径(一直向下) 
4. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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  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 ]; } 
5. 空间优化 观察状态转移方程,dp[i][j] 只依赖于同一行的前一个元素和上一行的同一个元素,因此可以使用一维数组优化:
1 2 3 4 5 6 7 8 9 10 11 12 public  int  uniquePaths (int  m, int  n)  {    int [] dp = new  int [n];     Arrays.fill(dp, 1 );          for  (int  i  =  1 ; i < m; i++) {         for  (int  j  =  1 ; j < n; j++) {             dp[j] = dp[j] + dp[j-1 ];         }     }          return  dp[n-1 ]; } 
解法二:组合数学 数学分析 从 (0,0) 到 (m-1,n-1),机器人需要:
向右移动 n-1 步 
向下移动 m-1 步 
总共移动 m+n-2 步 
 
问题转化为:在 m+n-2 个位置中选择 m-1 个位置向下移动(或选择 n-1 个位置向右移动)。
这是一个组合数问题:C(m+n-2, m-1) 或 C(m+n-2, n-1)
代码实现 1 2 3 4 5 6 7 public  int  uniquePaths (int  m, int  n)  {    long  result  =  1 ;     for  (int  i  =  1 ; i <= Math.min(m-1 , n-1 ); i++) {         result = result * (m + n - 1  - i) / i;     }     return  (int ) result; } 
经典例题二:不同路径II(带障碍物) 问题描述(带障碍物) LeetCode 63. 不同路径 II 
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
1 2 3 4 5 6 7 示例: 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右 
解法:动态规划(带障碍物) 状态转移方程(带障碍物) 1 2 3 4 如果 obstacleGrid[i][j] == 1:     dp[i][j] = 0  // 有障碍物,无法到达 否则:     dp[i][j] = dp[i-1][j] + dp[i][j-1] 
代码实现(带障碍物) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public  int  uniquePathsWithObstacles (int [][] obstacleGrid)  {    int  m  =  obstacleGrid.length;     int  n  =  obstacleGrid[0 ].length;               if  (obstacleGrid[0 ][0 ] == 1  || obstacleGrid[m-1 ][n-1 ] == 1 ) {         return  0 ;     }          int [][] dp = new  int [m][n];     dp[0 ][0 ] = 1 ;               for  (int  j  =  1 ; j < n; j++) {         dp[0 ][j] = (obstacleGrid[0 ][j] == 1 ) ? 0  : dp[0 ][j-1 ];     }               for  (int  i  =  1 ; i < m; i++) {         dp[i][0 ] = (obstacleGrid[i][0 ] == 1 ) ? 0  : dp[i-1 ][0 ];     }               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 ]; } 
空间优化版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public  int  uniquePathsWithObstacles (int [][] obstacleGrid)  {    int  n  =  obstacleGrid[0 ].length;     int [] dp = new  int [n];     dp[0 ] = obstacleGrid[0 ][0 ] == 1  ? 0  : 1 ;          for  (int [] row : obstacleGrid) {         for  (int  j  =  0 ; j < n; j++) {             if  (row[j] == 1 ) {                 dp[j] = 0 ;             } else  if  (j > 0 ) {                 dp[j] += dp[j-1 ];             }         }     }          return  dp[n-1 ]; } 
经典例题三:最小路径和 问题描述(最小路径和) LeetCode 64. 最小路径和 
给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
1 2 3 4 示例: 输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。 
解法:动态规划 状态定义 dp[i][j] 表示从 (0,0) 到 (i,j) 的最小路径和。
状态转移方程 1 dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) 
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public  int  minPathSum (int [][] grid)  {    int  m  =  grid.length;     int  n  =  grid[0 ].length;          int [][] dp = new  int [m][n];     dp[0 ][0 ] = grid[0 ][0 ];               for  (int  j  =  1 ; j < n; j++) {         dp[0 ][j] = dp[0 ][j-1 ] + grid[0 ][j];     }               for  (int  i  =  1 ; i < m; i++) {         dp[i][0 ] = dp[i-1 ][0 ] + grid[i][0 ];     }               for  (int  i  =  1 ; i < m; i++) {         for  (int  j  =  1 ; j < n; j++) {             dp[i][j] = grid[i][j] + Math.min(dp[i-1 ][j], dp[i][j-1 ]);         }     }          return  dp[m-1 ][n-1 ]; } 
原地修改版本(空间优化) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public  int  minPathSum (int [][] grid)  {    int  m  =  grid.length;     int  n  =  grid[0 ].length;               for  (int  j  =  1 ; j < n; j++) {         grid[0 ][j] += grid[0 ][j-1 ];     }               for  (int  i  =  1 ; i < m; i++) {         grid[i][0 ] += grid[i-1 ][0 ];     }               for  (int  i  =  1 ; i < m; i++) {         for  (int  j  =  1 ; j < n; j++) {             grid[i][j] += Math.min(grid[i-1 ][j], grid[i][j-1 ]);         }     }          return  grid[m-1 ][n-1 ]; } 
路径计数问题的通用思路 1. 问题识别 路径计数问题通常具有以下特征:
在网格或图中寻找路径 
移动方向有限制 
需要计算路径数量或找到最优路径 
 
2. 解题步骤 
状态定义 :通常定义 dp[i][j] 表示到达位置 (i,j) 的某种最优值状态转移 :根据可能的移动方向建立转移方程边界处理 :初始化第一行和第一列空间优化 :根据依赖关系优化空间复杂度 
3. 常见变形 
不同移动方向 :四个方向、八个方向带权重的路径 :最小/最大路径和路径记录 :不仅求最优值,还要构造具体路径多起点/终点 :从多个起点到多个终点的路径 
相关延伸问题 掌握了基本的路径计数问题后,可以尝试以下进阶题目:
LeetCode 120. 三角形最小路径和 
LeetCode 931. 下降路径最小和 
LeetCode 174. 地下城游戏 
LeetCode 1301. 最大得分的路径数目 
 
算法复杂度分析 时间复杂度 
基本路径计数:O(m × n) 
空间优化版本:O(m × n) 
 
空间复杂度 
二维DP:O(m × n) 
一维DP优化:O(min(m, n)) 
原地修改:O(1) 
 
总结 路径计数问题是动态规划的重要应用领域,它体现了DP在组合计数问题中的强大能力:
状态设计简洁 :通常以坐标作为状态维度转移逻辑清晰 :基于移动方向的限制优化空间灵活 :可以根据依赖关系进行空间优化应用场景广泛 :从纯计数到最优化问题 
通过掌握这类问题的解法,我们不仅学会了具体的算法技巧,更重要的是理解了动态规划在空间搜索问题中的应用模式。
系列文章回顾 
推荐练习 
在LeetCode上练习更多路径相关的DP问题 
尝试实现带路径记录的版本 
思考路径问题在实际项目中的应用场景