问题引入
想象你站在一个 m × n 网格的左上角,目标是通过只能向右或向下移动的方式,到达右下角的终点。这是一个经典的组合计数问题,也是动态规划入门的最佳案例之一。本文将从数学组合公式与递推动态规划两个角度,用Java完整实现路径计数算法,并扩展到带障碍物的网格场景。
算法原理
方法一:组合数学
从左上角 (0, 0) 走到右下角 (m-1, n-1),总共需要走 (m-1) 步向下和 (n-1) 步向右,总计 (m+n-2) 步。问题转化为:在这 (m+n-2) 步中选择哪些步向下(或向右),方案数为组合数:
C(m+n-2, m-1) = (m+n-2)! / [(m-1)! × (n-1)!]
直接计算阶乘容易溢出,需使用递推公式或动态规划计算组合数。
方法二:动态规划
设 dp[i][j] 表示从起点到达 (i, j) 的路径数。状态转移方程为:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:dp[0][j] = 1(第一行只能向右),dp[i][0] = 1(第一列只能向下)。
扩展:带障碍物的不同路径II
若网格中存在障碍物(标记为 1),则障碍物格子的 dp 值为 0,其余转移逻辑不变。
Java完整实现
import java.util.Arrays;
/**
* 网格路径计数算法实现
* 包含组合数学、动态规划及带障碍物扩展三种解法
*/
public class GridPath {
/**
* 方法一:组合数学计算路径数
* 时间复杂度 O(min(m,n)),空间复杂度 O(1)
* 使用递推公式 C(a,b) = C(a,b-1) * (a-b+1) / b 避免溢出
*/
public long uniquePathsMath(int m, int n) {
// 确保 a >= b,减少计算量
int a = m + n - 2;
int b = Math.min(m - 1, n - 1);
long result = 1;
for (int i = 1; i <= b; i++) {
result = result * (a - b + i) / i;
}
return result;
}
/**
* 方法二:二维动态规划
* 时间复杂度 O(m×n),空间复杂度 O(m×n)
*/
public int uniquePathsDP(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];
}
/**
* 方法三:滚动数组优化动态规划
* 时间复杂度 O(m×n),空间复杂度 O(n)
* 只保留当前行和上一行的状态
*/
public int uniquePathsOptimized(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1); // 第一行所有列的路径数均为1
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // dp[j]来自上方,dp[j-1]来自左方
}
}
return dp[n - 1];
}
/**
* 扩展:带障碍物的不同路径II
* obstacleGrid[i][j] == 1 表示障碍物
* 时间复杂度 O(m×n),空间复杂度 O(n)
*/
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[] dp = new int[n];
// 起点若为障碍物则直接返回0
dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0; // 障碍物位置不可达
} else if (j > 0) {
dp[j] += dp[j - 1]; // 累加左方路径数
}
// j==0时dp[0]保持原值(来自上方)
}
}
return dp[n - 1];
}
/**
* 打印网格路径的DP表格(调试用)
*/
public void printDPTable(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];
}
}
System.out.println("DP路径数表格 (" + m + "x" + n + "):");
for (int i = 0; i < m; i++) {
System.out.println(Arrays.toString(dp[i]));
}
}
public static void main(String[] args) {
GridPath solution = new GridPath();
int m = 3, n = 7;
System.out.println("=== 网格路径计数算法演示 ===");
System.out.println("网格大小: " + m + " x " + n);
System.out.println("组合数学结果: " + solution.uniquePathsMath(m, n));
System.out.println("二维DP结果: " + solution.uniquePathsDP(m, n));
System.out.println("滚动数组优化结果: " + solution.uniquePathsOptimized(m, n));
solution.printDPTable(m, n);
// 带障碍物测试
int[][] obstacleGrid = {
{0, 0, 0},
{0, 1, 0},
{0, 0, 0}
};
System.out.println("\n带障碍物路径数 (3x3, 中间有障碍): "
+ solution.uniquePathsWithObstacles(obstacleGrid));
}
}
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 组合数学 | O(min(m,n)) | O(1) | 无障碍物,仅需计数 |
| 二维DP | O(m×n) | O(m×n) | 需要路径表格或调试 |
| 滚动数组 | O(m×n) | O(n) | 无障碍物,空间敏感 |
| 障碍物扩展 | O(m×n) | O(n) | 存在障碍物的通用场景 |
扩展思考
- 路径输出:若需输出所有具体路径而非仅计数,可使用DFS回溯,配合StringBuilder记录路径方向。
- 最小路径和:若网格中每个格子有权值,可改为记录最小/最大路径和,状态转移取
min(dp[i-1][j], dp[i][j-1]) + grid[i][j]。 - 三维扩展:在三维网格中从
(0,0,0)走到(x,y,z),只能沿正方向移动,方案数为C(x+y+z, x) × C(y+z, y)。
网格路径问题是理解动态规划”最优子结构”与”无后效性”特性的绝佳入口,也是许多高级算法(如数字三角形、矩阵链乘)的思维基石。