题目分析
机器人在一个 m x n
的网格上移动,起点在左上角 (0,0)
,终点在右下角 (m-1,n-1)
。机器人每次只能向下或者右移动一步,问题是:有多少种不同的路径可以从起点到达终点?
思考要点:
- 机器人只能向下和向右移动,这意味着路径的选择受限,实际上就是在一个固定的路径集合中找到所有可能的路径。
- 我们可以通过多种方式来计算路径的总数,比较经典的解法包括递归法、动态规划法、以及组合数学法。接下来,我将逐一讲解这些方法。
解题思路一:递归法
递归是解决此类问题的基础方法。通过递归将问题分解为更小的子问题,最终汇总子问题的结果来得出解答。
- 递归关系:当机器人在位置
(i, j)
时,它可以从(i-1, j)
或者(i, j-1)
走到当前位置。因此,该位置的路径总数等于从它上方和左方来的路径数之和。 - 边界条件:
- 当机器人位于第一行时,只能从左边来(向右走)。
- 当机器人位于第一列时,只能从上边来(向下走)。
- 递归终止条件:
- 当到达右下角时,递归结束,此时返回1条路径。
代码实现(递归)
#include <iostream>
using namespace std;
int uniquePaths(int m, int n) {
if (m == 1 || n == 1) {
return 1;
}
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
int main() {
int m = 3, n = 7;
cout << "不同路径数: " << uniquePaths(m, n) << endl;
return 0;
}
缺点分析:
- 这种纯递归法存在效率问题。由于递归会重复计算相同的子问题,时间复杂度为指数级别 O ( 2 m + n ) O(2^{m+n}) O(2m+n),尤其在网格较大时,会导致超时。
改进:记忆化递归
为了解决递归法的效率问题,我们可以使用记忆化递归。通过存储中间结果,避免重复计算相同的子问题,使得时间复杂度大幅降低至 O(m * n)
。
代码实现(记忆化递归)
#include <iostream>
#include <vector>
using namespace std;
int helper(int m, int n, vector<vector<int>>& memo) {
if (m == 1 || n == 1) {
return 1;
}
if (memo[m][n] != -1) {
return memo[m][n];
}
memo[m][n] = helper(m - 1, n, memo) + helper(m, n - 1, memo);
return memo[m][n];
}
int uniquePaths(int m, int n) {
vector<vector<int>> memo(m + 1, vector<int>(n + 1, -1));
return helper(m, n, memo);
}
int main() {
int m = 3, n = 7;
cout << "不同路径数: " << uniquePaths(m, n) << endl;
return 0;
}
解题思路二:动态规划法
在递归法中,我们看到大量重复计算的情况。动态规划的核心思想是使用表格存储中间结果,逐步填充表格来得到最终结果,从而避免递归中的重复计算问题。
- 状态定义:
dp[i][j]
表示从起点(0, 0)
到位置(i, j)
的路径数。 - 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
,即该位置的路径数等于从它的上方和左方来的路径数之和。 - 初始条件:
- 第一行和第一列的路径数为1,因为这些位置只能从一个方向走到。
- 边界条件:只需考虑网格边界外的路径初始化。
代码实现(动态规划)
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(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];
}
int main() {
int m = 3, n = 7;
cout << "不同路径数: " << uniquePaths(m, n) << endl;
return 0;
}
动态规划的优化:滚动数组
- 动态规划法的空间复杂度是
O(m * n)
,但实际上我们只需要存储当前行和上一行的值。因此,我们可以用滚动数组将空间复杂度优化为O(n)
。
滚动数组优化后的代码:
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
vector<int> dp(n, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1];
}
}
return dp[n - 1];
}
int main() {
int m = 3, n = 7;
cout << "不同路径数: " << uniquePaths(m, n) << endl;
return 0;
}
解题思路三:组合数学法
除了动态规划,还可以使用组合数学的方法解决。机器人需要总共走 m-1
步向下和 n-1
步向右,总共需要走 m+n-2
步。因此,问题可以转化为从这些步数中选择 m-1
步向下的组合数问题。
- 组合数公式为:
C ( m + n − 2 , m − 1 ) = ( m + n − 2 ) ! ( m − 1 ) ! ( n − 1 ) ! C(m+n-2, m-1) = \frac{(m+n-2)!}{(m-1)!(n-1)!} C(m+n−2,m−1)=(m−1)!(n−1)!(m+n−2)! - 优化建议:由于直接计算阶乘容易导致溢出,我们可以通过迭代计算组合数,避免溢出。
代码实现(组合数学法)
#include <iostream>
using namespace std;
long long combination(int m, int n) {
long long result = 1;
int k = min(m, n);
for (int i = 1; i <= k; i++) {
result = result * (m + n - i) / i;
}
return result;
}
int uniquePaths(int m, int n) {
return combination(m - 1, n - 1);
}
int main() {
int m = 3, n = 7;
cout << "不同路径数: " << uniquePaths(m, n) << endl;
return 0;
}
代码效率分析
-
递归法:
- 时间复杂度:
O(2^{m+n})
,由于大量重复计算,效率最低。 - 空间复杂度:
O(m+n)
,由于递归调用栈占用空间。
- 时间复杂度:
-
记忆化递归法和动态规划法:
- 时间复杂度:
O(m*n)
,通过存储中间结果,显著减少了重复计算。 - 空间复杂度:记忆化递归和普通动态规划都是
O(m*n)
,滚动数组优化的动态规划则是O(n)
。
- 时间复杂度:
-
组合数学法:
- 时间复杂度:
O(m+n)
,通过组合数公式快速计算路径数。 - 空间复杂度:
O(1)
,仅需常数级空间存储结果。
- 时间复杂度:
总结
- 递归法适合初学者理解递归和问题分解的思想,但效率不高。
- 动态规划法是解决此类问题的常用方法,通过表格存储中间结果,能处理较大规模数据。
- 组合数学法直接运用组合数公式,提供最优解,时间和空间复杂度都最小,但需要一定的数学基础。