C++ 动态规划(Dynamic Programming)详解:从理论到实战
动态规划(Dynamic Programming,简称 DP)是一种通过将复杂问题分解为重叠子问题,并利用子问题的解来高效求解原问题的算法思想。它在优化问题、组合计数、路径规划等领域有着广泛应用,尤其适合解决具有重叠子问题和最优子结构特性的问题。本文将从动态规划的核心思想出发,结合 C++ 实现,深入解析动态规划的设计与应用。
一、动态规划的核心思想
动态规划的本质是避免重复计算,通过存储中间子问题的解(即 "记忆化")来提高算法效率。其核心要素包括:
1.1 重叠子问题(Overlapping Subproblems)
问题可以分解为多个重复出现的子问题。例如,斐波那契数列中,fib(n) = fib(n-1) + fib(n-2),计算fib(5)时需要重复计算fib(3)等子问题。
1.2 最优子结构(Optimal Substructure)
问题的最优解包含子问题的最优解。例如,最短路径问题中,从 A 到 C 的最短路径必然包含从 A 到中间点 B 的最短路径。
1.3 状态转移方程(State Transition)
用数学公式描述问题状态之间的关系,是动态规划的核心。例如,斐波那契数列的状态转移方程为:
plaintext
dp[n] = dp[n-1] + dp[n-2]
二、动态规划的两种实现方式
动态规划通常有两种实现形式,分别适用于不同场景:
2.1 备忘录法(Memoization,自顶向下)
- 思想:递归解决策过程,缓存子问题的解以避免重复计算。
- 适用场景:子问题数量不确定,或递归逻辑更直观的问题。
示例:斐波那契数列(备忘录法)
cpp
运行
#include <iostream>
#include <vector>using namespace std;// 备忘录存储子问题的解
vector<int> memo;int fib(int n) {if (n <= 1) return n;// 若已计算过,直接返回缓存结果if (memo[n] != -1) return memo[n];// 否则计算并缓存memo[n] = fib(n-1) + fib(n-2);return memo[n];
}int main() {int n = 10;memo.resize(n+1, -1); // 初始化备忘录cout << "fib(" << n << ") = " << fib(n) << endl; // 输出55return 0;
}
2.2 迭代法(自底向上)
- 思想:从最小的子问题开始,逐步向上计算更大的子问题,直至得到原问题的解。
- 适用场景:子问题结构清晰,可按顺序迭代计算,空间效率通常更高。
示例:斐波那契数列(迭代法)
cpp
运行
#include <iostream>
#include <vector>using namespace std;int fib(int n) {if (n <= 1) return n;// dp数组存储子问题的解vector<int> dp(n+1);dp[0] = 0;dp[1] = 1;// 从子问题逐步计算for (int i = 2; i <= n; ++i) {dp[i] = dp[i-1] + dp[i-2];}return dp[n];
}int main() {cout << "fib(10) = " << fib(10) << endl; // 输出55return 0;
}
空间优化:观察到计算dp[i]仅需dp[i-1]和dp[i-2],可压缩空间至 O (1):
cpp
运行
int fib(int n) {if (n <= 1) return n;int a = 0, b = 1, c;for (int i = 2; i <= n; ++i) {c = a + b;a = b;b = c;}return b;
}
三、经典动态规划问题与 C++ 实现
3.1 爬楼梯问题(简单)
问题:一次可以爬 1 或 2 级台阶,求爬到第 n 级的不同方法数。
分析:
- 状态定义:
dp[i]表示爬到第 i 级的方法数。 - 转移方程:
dp[i] = dp[i-1] + dp[i-2](最后一步爬 1 级或 2 级)。 - 边界条件:
dp[0] = 1(起点),dp[1] = 1。
实现:
cpp
运行
int climbStairs(int n) {if (n <= 1) return 1;int a = 1, b = 1, c;for (int i = 2; i <= n; ++i) {c = a + b;a = b;b = c;}return b;
}
3.2 最长回文子串(中等)
问题:找出字符串中最长的回文子串。
分析:
- 状态定义:
dp[i][j]表示s[i..j]是否为回文串。 - 转移方程:
dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]。 - 边界条件:长度为 1 的子串是回文(
dp[i][i] = true);长度为 2 的子串若两字符相等则是回文。
实现:
cpp
运行
#include <iostream>
#include <string>
#include <vector>using namespace std;string longestPalindrome(string s) {int n = s.size();if (n == 0) return "";vector<vector<bool>> dp(n, vector<bool>(n, false));int start = 0, max_len = 1;// 长度为1的回文for (int i = 0; i < n; ++i) {dp[i][i] = true;}// 长度为2的回文for (int i = 0; i < n-1; ++i) {if (s[i] == s[i+1]) {dp[i][i+1] = true;start = i;max_len = 2;}}// 长度>=3的回文for (int len = 3; len <= n; ++len) { // 子串长度for (int i = 0; i <= n - len; ++i) { // 起始索引int j = i + len - 1; // 结束索引if (s[i] == s[j] && dp[i+1][j-1]) {dp[i][j] = true;if (len > max_len) {start = i;max_len = len;}}}}return s.substr(start, max_len);
}int main() {string s = "babad";cout << longestPalindrome(s) << endl; // 输出"bab"或"aba"return 0;
}
3.3 0-1 背包问题(中等)
问题:有 n 件物品,每件物品有重量w[i]和价值v[i],背包最大承重为 C,求能装入背包的最大价值。
分析:
- 状态定义:
dp[i][j]表示前 i 件物品中,承重为 j 时的最大价值。 - 转移方程:
- 不选第 i 件:
dp[i][j] = dp[i-1][j] - 选第 i 件(若承重允许):
dp[i][j] = dp[i-1][j-w[i]] + v[i] - 取两者最大值:
dp[i][j] = max(不选, 选)
- 不选第 i 件:
- 边界条件:
dp[0][j] = 0(无物品时价值为 0),dp[i][0] = 0(承重为 0 时价值为 0)。
实现(空间优化版):
cpp
运行
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int knapsack(vector<int>& w, vector<int>& v, int C) {int n = w.size();if (n == 0 || C == 0) return 0;// 优化为一维数组,空间复杂度O(C)vector<int> dp(C + 1, 0);for (int i = 0; i < n; ++i) {// 从后往前更新,避免覆盖未使用的子问题解for (int j = C; j >= w[i]; --j) {dp[j] = max(dp[j], dp[j - w[i]] + v[i]);}}return dp[C];
}int main() {vector<int> w = {2, 3, 4, 5}; // 重量vector<int> v = {3, 4, 5, 6}; // 价值int C = 8; // 最大承重cout << "最大价值: " << knapsack(w, v, C) << endl; // 输出10return 0;
}
3.4 最长递增子序列(LIS,中等)
问题:求数组中最长的严格递增子序列的长度。
分析:
- 状态定义:
dp[i]表示以nums[i]结尾的最长递增子序列长度。 - 转移方程:
dp[i] = max(dp[j] + 1)对所有j < i且nums[j] < nums[i]。 - 边界条件:
dp[i] = 1(每个元素自身是长度为 1 的子序列)。
实现:
cpp
运行
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;int lengthOfLIS(vector<int>& nums) {int n = nums.size();if (n == 0) return 0;vector<int> dp(n, 1);int max_len = 1;for (int i = 1; i < n; ++i) {for (int j = 0; j < i; ++j) {if (nums[j] < nums[i]) {dp[i] = max(dp[i], dp[j] + 1);}}max_len = max(max_len, dp[i]);}return max_len;
}int main() {vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};cout << "最长递增子序列长度: " << lengthOfLIS(nums) << endl; // 输出4(如2,3,7,18)return 0;
}
3.5 编辑距离(困难)
问题:求将字符串word1转换为word2的最少操作次数(插入、删除、替换)。
分析:
- 状态定义:
dp[i][j]表示将word1[0..i-1]转换为word2[0..j-1]的最少操作数。 - 转移方程:
- 若
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](无需操作) - 否则:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1(分别对应删除、插入、替换)
- 若
- 边界条件:
dp[i][0] = i(删除 i 个字符),dp[0][j] = j(插入 j 个字符)。
实现:
cpp
运行
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>using namespace std;int minDistance(string word1, string word2) {int m = word1.size(), n = word2.size();vector<vector<int>> dp(m+1, vector<int>(n+1, 0));// 边界条件for (int i = 0; i <= m; ++i) dp[i][0] = i;for (int j = 0; j <= n; ++j) dp[0][j] = j;// 填充dp表for (int i = 1; i <= m; ++i) {for (int j = 1; j <= n; ++j) {if (word1[i-1] == word2[j-1]) {dp[i][j] = dp[i-1][j-1];} else {dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;}}}return dp[m][n];
}int main() {string word1 = "horse", word2 = "ros";cout << "最少操作次数: " << minDistance(word1, word2) << endl; // 输出3return 0;
}
四、动态规划的优化技巧
4.1 空间优化
- 滚动数组:当状态转移仅依赖前一行(或前几行)时,可用一维数组替代二维数组(如 0-1 背包问题)。
- 变量替代:若仅依赖前一个状态,可直接用变量存储(如斐波那契数列)。
4.2 状态压缩
- 合并冗余状态,减少维度或状态数量。例如,某些问题中
dp[i][j]可简化为dp[j]。
4.3 斜率优化
- 针对线性转移方程(如
dp[i] = min(dp[j] + a[i] * b[j])),通过维护单调队列优化时间复杂度(适用于高级问题)。
五、动态规划的常见误区
- 过度设计状态:状态定义应简洁明了,避免包含无关信息。
- 忽略边界条件:边界条件是动态规划的基础,需仔细处理(如空字符串、索引越界等)。
- 混淆状态转移方向:自底向上时需确保子问题已先求解,自顶向下时需正确设置递归终止条件。
- 未优化空间:高维 DP 问题(如三维)需及时压缩空间,否则可能导致内存溢出。
六、总结
动态规划是一种 "以空间换时间" 的算法思想,其核心在于识别问题的重叠子问题和最优子结构,并通过状态转移方程将问题分解为可逐步求解的子问题。从简单的斐波那契数列到复杂的编辑距离,动态规划提供了高效的解决方案。
在 C++ 实现中,需根据问题特性选择备忘录法或迭代法,并注意空间优化以提升效率。掌握动态规划不仅能解决各类算法问题,更能培养 "分解问题、抽象状态" 的思维能力,这在复杂系统设计中同样至关重要。
动态规划的学习需要大量实践,建议从经典问题入手,逐步积累对状态设计和转移方程的敏感度,最终达到灵活应用的水平。
