动态规划算法简介
动态规划(Dynamic Programming, DP)是一种将复杂问题分解为更简单的子问题来求解的算法思想。它通过保存中间子问题的解,避免了重复计算,从而大大提高了解决问题的效率。动态规划通常用于求解最优化问题,比如最短路径、最大收益等。
动态规划解题步骤
- 确定状态:明确在问题的某一步中,需要存储什么信息来描述子问题的解。
- 状态转移方程:找出如何通过前一步的状态来得到当前状态,即如何递推地求解问题。
- 初始状态:确定最基本的状态,即最小子问题的解。
- 计算结果:通过状态转移逐步得到问题的最终解。
递归与动态规划的对比
为了更好地理解动态规划的优势,我们以“爬楼梯”问题为例,展示递归方法与动态规划方法的对比。
1. 爬楼梯问题(一维)
假设你正在爬楼梯,需要爬 n
阶才能到达楼顶。每次你可以爬 1
或 2
个台阶。问有多少种不同的方法可以爬到楼顶?
递归解法:
class Solution {
public:
int climbStairs(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
if(n == 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
}
};
递归方法虽然直观,但由于存在大量重复计算,导致时间复杂度较高(O(2^n)
),在 n
较大时会超时。
动态规划解法:
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n+1);
if(n <= 1) return 1;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
动态规划方法通过自底向上地保存子问题的解,避免了递归中的重复计算。其时间复杂度为 O(n)
,空间复杂度也可以进一步优化到 O(1)
。
机器人走路问题(二维)
题目描述:
一个机器人位于一个 m x n
网格的左上角(起始点标记为“Start”)。机器人每次只能向下或者向右移动一步,目标是达到网格的右下角(标记为“Finish”)。问总共有多少条不同的路径?
动态规划解法:
解题思路:
- 状态定义:定义一个二维数组
dp[i][j]
,表示机器人从左上角走到(i, j)
的不同路径数。 - 状态转移方程:对于每个格子
(i, j)
,机器人可以从上方(i-1, j)
或左方(i, j-1)
到达,因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 - 初始状态:在边界上,机器人只能从一个方向到达,因此
dp[i][1] = 1
,dp[1][j] = 1
。 - 最终结果:目标是求解右下角
dp[m][n]
。
代码实现:
class Solution {
public:
int uniquePaths(int m, int n) {
int dp[m+1][n+1];
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if(i == 1 || j == 1) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
}
return dp[m][n];
}
};
代码解读:
- 二维DP数组:
dp[m+1][n+1]
用于存储从起点到各个位置的路径数目。 - 边界条件:因为机器人只能向右或向下移动,因此边界上的路径数目为
1
。 - 状态转移方程:对于非边界点,路径数目等于上方和左方路径数目的和。
- 时间复杂度与空间复杂度:该算法的时间复杂度为
O(m * n)
,可以通过仅使用一维数组优化空间复杂度到O(min(m, n))
。
结论
通过这两个经典问题的分析,我们看到动态规划方法在处理复杂问题时的强大之处。相比于递归,动态规划通过存储子问题的解,有效减少了重复计算,显著提升了算法的效率。因此,在面临类似的问题时,动态规划通常是更为优越的选择。