跳到主要内容
C++动态规划:从暴力搜索到最优解 | 极客日志
C++ 算法
C++动态规划:从暴力搜索到最优解 综述由AI生成 C++ 动态规划的核心概念、三大特征及两种实现方式(记忆化搜索与递推)。涵盖了线性 DP、背包 DP、区间 DP、状态压缩 DP、树形 DP 和数位 DP 六大经典模型,并提供了最长递增子序列、0-1 背包、石子合并等例题的代码实现。此外还讲解了空间优化、斜率优化等技巧以及五步解题法,帮助读者掌握从暴力搜索到最优解的算法思维。
moshang 发布于 2026/3/23 更新于 2026/5/29 9.8K 浏览第一章:动态规划的前世今生——一个思想的诞生
1.1 从斐波那契说起:为什么需要 DP?
让我们从一个经典的故事开始。1202 年,意大利数学家斐波那契提出了著名的兔子问题,由此诞生了斐波那契数列。800 年后,当我们在计算机上计算这个数列时,却遇到了麻烦。
int fib (int n) {
if (n <= 1 ) return n;
return fib (n-1 ) + fib (n-2 );
}
看起来很美,对吧?但当我尝试计算 fib(50) 时,计算机开始发出痛苦的呻吟。让我们看看发生了什么:
fib (5 ) = fib (4 ) + fib (3 ) = (fib(3 ) + fib (2 )) + (fib(2 ) + fib (1 )) = ((fib(2 ) + fib (1 )) + (fib(1 ) + fib (0 ))) + ((fib(1 ) + fib (0 )) + fib (1 )) = ...
惊人的事实 :计算 fib(5) 需要调用函数 15 次,而 fib(50) 需要约 400 亿次调用!为什么?因为我们在重复计算相同的东西:fib(3) 被计算了 2 次,fib(2) 被计算了 3 次...
1.2 DP 的核心理念:记住你的答案
动态规划(Dynamic Programming)的思想很简单却深刻:如果一个问题可以分解为重叠的子问题,那就记住每个子问题的解,避免重复计算 。
1950 年代,理查德·贝尔曼在兰德公司研究多阶段决策过程时,创造了'动态规划'这个术语。他后来说:"'动态'是因为时间是重要的组成部分,'规划'是因为我们在优化一个计划。"
第二章:DP 的三大特征——识别 DP 问题的火眼金睛
2.1 特征一:最优子结构(Optimal Substructure) 一个大问题的最优解包含其子问题的最优解。就像搭积木,整体的最优结构由局部的最优结构组成。
经典例子:最短路径问题
如果从 A 到 C 的最短路径经过 B,那么 A 到 B 和 B 到 C 的路径也必定是最短的。
最短路径(A, C) = 最短路径(A, B) + 最短路径(B, C)
2.2 特征二:重叠子问题(Overlapping Subproblems) 不同的大问题会共享相同的小问题。这是我们能'记忆化'的基础。
fib (5 ) 需要 fib (3 )
fib (4 ) 也需要 fib (3 )
2.3 特征三:无后效性(No Aftereffect) 当前状态一旦确定,后续决策不受之前决策影响 。这是 DP 能够顺序计算的关键。
想象你在玩迷宫游戏:你当前的位置决定了下一步的选择,但不需要知道你是怎么走到这里的。
第三章:DP 的两种实现方式——自顶向下与自底向上
3.1 方法一:记忆化搜索(Memoization,自顶向下) #include <iostream>
#include <vector>
using namespace std;
int fib_memo (int n, vector<int >& memo) {
if (n <= 1 ) return n;
if (memo[n] != -1 ) {
return memo[n];
}
memo[n] = fib_memo (n-1 , memo) + fib_memo (n-2 , memo);
return memo[n];
}
int fib_memoization (int n) {
vector<int > memo (n + 1 , -1 ) ;
return fib_memo (n, memo);
}
3.2 方法二:递推(Tabulation,自底向上)
int fib_tabulation (int n) {
if (n <= 1 ) return n;
vector<int > dp (n + 1 , 0 ) ;
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 fib_optimized (int n) {
if (n <= 1 ) return n;
int prev2 = 0 ;
int prev1 = 1 ;
int current;
for (int i = 2 ; i <= n; i++) {
current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return current;
}
第四章:DP 问题分类详解——六大经典模型
4.1 模型一:线性 DP
例题 1:最长递增子序列(LIS) 状态定义 :dp[i] = 以 nums[i] 结尾的最长递增子序列长度
状态转移 :dp[i] = max(dp[j]) + 1,对所有 j < i 且 nums[j] < nums[i]
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int lengthOfLIS (vector<int >& nums) {
if (nums.empty ()) return 0 ;
int n = nums.size ();
vector<int > dp (n, 1 ) ;
int maxLen = 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 );
}
}
maxLen = max (maxLen, dp[i]);
}
return maxLen;
}
int lengthOfLIS_optimized (vector<int >& nums) {
vector<int > tails;
for (int num : nums) {
auto it = lower_bound (tails.begin (), tails.end (), num);
if (it == tails.end ()) {
tails.push_back (num);
} else {
*it = num;
}
}
return tails.size ();
}
4.2 模型二:背包 DP
例题 2:0-1 背包问题 问题:有 n 件物品,第 i 件物品重量 w[i],价值 v[i],背包容量 C。每件物品只能选 0 或 1 次,求最大价值。
状态定义 :dp[i][j] = 前 i 件物品放入容量为 j 的背包的最大价值
dp[i] [j] = max(
dp[i-1] [j] , // 不选第 i 件
dp[i-1] [j-w[i] ] + v[i] // 选第 i 件 (如果 j ≥ w[i] )
)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int knapsack_2D (int C, vector<int >& w, vector<int >& v) {
int n = w.size ();
vector<vector<int >> dp (n + 1 , vector <int >(C + 1 , 0 ));
for (int i = 1 ; i <= n; i++) {
for (int j = 1 ; j <= C; j++) {
dp[i][j] = dp[i-1 ][j];
if (j >= w[i-1 ]) {
dp[i][j] = max (dp[i][j], dp[i-1 ][j-w[i-1 ]] + v[i-1 ]);
}
}
}
return dp[n][C];
}
int knapsack_1D (int C, vector<int >& w, vector<int >& v) {
int n = w.size ();
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 > weights = {2 , 3 , 4 , 5 };
vector<int > values = {3 , 4 , 5 , 6 };
int capacity = 8 ;
cout << "最大价值 (2D): " << knapsack_2D (capacity, weights, values) << endl;
cout << "最大价值 (1D): " << knapsack_1D (capacity, weights, values) << endl;
return 0 ;
}
背包问题变种对比表: 问题类型 特点 状态转移关键 0-1 背包 每件物品选 0 或 1 次 从后往前遍历 完全背包 每件物品无限次 从前往后遍历 多重背包 每件物品有限次 二进制拆分优化 分组背包 每组最多选一件 先遍历容量再遍历组内物品
4.3 模型三:区间 DP
例题 3:石子合并问题 问题:N 堆石子排成一排,每次合并相邻两堆,代价为两堆石子数之和,求合并成一堆的最小总代价。
状态定义 :dp[i][j] = 合并第 i 到第 j 堆石子的最小代价
dp[i] [j] = min(dp[i] [k] + dp[k+1] [j] ) + sum[i] [j]
其中 i ≤ k < j
sum[i] [j] 是第 i 到第 j 堆石子总数
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int stoneMerge (vector<int >& stones) {
int n = stones.size ();
if (n == 0 ) return 0 ;
vector<int > prefix (n + 1 , 0 ) ;
for (int i = 1 ; i <= n; i++) {
prefix[i] = prefix[i-1 ] + stones[i-1 ];
}
vector<vector<int >> dp (n, vector <int >(n, 0 ));
for (int len = 2 ; len <= n; len++) {
for (int i = 0 ; i + len - 1 < n; i++) {
int j = i + len - 1 ;
dp[i][j] = INT_MAX;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k+1 ][j] + (prefix[j+1 ] - prefix[i]);
dp[i][j] = min (dp[i][j], cost);
}
}
}
return dp[0 ][n-1 ];
}
int main () {
vector<int > stones = {4 , 5 , 9 , 4 };
cout << "最小合并代价:" << stoneMerge (stones) << endl;
return 0 ;
}
4.4 模型四:状态压缩 DP
例题 4:旅行商问题(TSP)简化版 问题:访问 n 个城市,每个城市恰好一次,最后回到起点,求最短路径。
状态定义 :dp[mask][i] = 当前访问过的城市集合为 mask,最后在城市的 i 的最小路径长度
其中 mask 用二进制表示,第 k 位为 1 表示城市 k 已访问
dp[mask] [i] = min(dp[prev_mask] [j] + dist[j] [i] )
其中 prev_mask = mask ^ (1 << i ),即上一次没在城市 i
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int tsp (vector<vector<int >>& dist) {
int n = dist.size ();
if (n == 0 ) return 0 ;
vector<vector<int >> dp (1 << n, vector <int >(n, INT_MAX / 2 ));
dp[1 ][0 ] = 0 ;
for (int mask = 1 ; mask < (1 << n); mask++) {
for (int i = 0 ; i < n; i++) {
if (dp[mask][i] == INT_MAX / 2 ) continue ;
for (int j = 0 ; j < n; j++) {
if (mask & (1 << j)) continue ;
int new_mask = mask | (1 << j);
dp[new_mask][j] = min (dp[new_mask][j], dp[mask][i] + dist[i][j]);
}
}
}
int ans = INT_MAX;
int full_mask = (1 << n) - 1 ;
for (int i = 1 ; i < n; i++) {
if (dp[full_mask][i] < INT_MAX / 2 && dist[i][0 ] < INT_MAX / 2 ) {
ans = min (ans, dp[full_mask][i] + dist[i][0 ]);
}
}
return ans;
}
int main () {
vector<vector<int >> dist = {
{0 , 10 , 15 , 20 },
{10 , 0 , 35 , 25 },
{15 , 35 , 0 , 30 },
{20 , 25 , 30 , 0 }
};
cout << "最短哈密顿回路长度:" << tsp (dist) << endl;
return 0 ;
}
4.5 模型五:树形 DP
例题 5:二叉树中的最大路径和 问题:在二叉树中找一条路径(节点序列),使得路径上节点值之和最大。
max_gain(node):以 node 为起点的向下路径的最大和
全局变量 max_sum 记录答案
max_gain (node) = node->val + max (0 , max_gain(left), max_gain (right))
max_sum = max (max_sum, node->val + max(0 , max_gain(left)) + max (0 , max_gain(right)))
#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode (int x) : val (x), left (nullptr ), right (nullptr ) {}
};
class Solution {
private :
int maxSum;
int maxGain (TreeNode* node) {
if (!node) return 0 ;
int leftGain = max (maxGain (node->left), 0 );
int rightGain = max (maxGain (node->right), 0 );
int priceNewpath = node->val + leftGain + rightGain;
maxSum = max (maxSum, priceNewpath);
return node->val + max (leftGain, rightGain);
}
public :
int maxPathSum (TreeNode* root) {
maxSum = INT_MIN;
maxGain (root);
return maxSum;
}
};
TreeNode* buildTestTree () {
TreeNode* root = new TreeNode (10 );
root->left = new TreeNode (2 );
root->right = new TreeNode (10 );
root->left->left = new TreeNode (20 );
root->left->right = new TreeNode (1 );
root->right->right = new TreeNode (-25 );
root->right->right->left = new TreeNode (3 );
root->right->right->right = new TreeNode (4 );
return root;
}
int main () {
Solution solution;
TreeNode* root = buildTestTree ();
cout << "二叉树最大路径和:" << solution.maxPathSum (root) << endl;
return 0 ;
}
4.6 模型六:数位 DP
例题 6:计算 1 到 n 中数字 x 出现的次数 问题:计算在 1 到 n 的所有整数中,数字 x(0-9)出现的次数。
状态定义 :dp[pos][cnt][limit][lead]
pos: 当前处理到的位数(从高位到低位)
cnt: 当前已统计到的数字 x 的出现次数
limit: 是否受到 n 的限制(当前位能否取 9)
lead: 是否有前导零
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
class DigitDP {
private :
vector<int > digits;
int dp[20 ][20 ][2 ][2 ];
int dfs (int pos, int cnt, bool limit, bool lead, int x) {
if (pos == digits.size ()) {
return cnt;
}
if (dp[pos][cnt][limit][lead] != -1 ) {
return dp[pos][cnt][limit][lead];
}
int ans = 0 ;
int up = limit ? digits[pos] : 9 ;
for (int d = 0 ; d <= up; d++) {
bool new_limit = limit && (d == up);
bool new_lead = lead && (d == 0 );
int new_cnt = cnt;
if (!new_lead || d != 0 ) {
if (d == x) {
new_cnt++;
}
}
ans += dfs (pos + 1 , new_cnt, new_limit, new_lead, x);
}
return dp[pos][cnt][limit][lead] = ans;
}
public :
int countDigitX (int n, int x) {
if (n <= 0 ) return 0 ;
digits.clear ();
while (n > 0 ) {
digits.push_back (n % 10 );
n /= 10 ;
}
reverse (digits.begin (), digits.end ());
memset (dp, -1 , sizeof (dp));
return dfs (0 , 0 , true , true , x);
}
};
int main () {
DigitDP digitDP;
int n = 12345 ;
int x = 2 ;
cout << "在 1 到" << n << "中,数字" << x << "出现了" << digitDP.countDigitX (n, x) << "次" << endl;
return 0 ;
}
第五章:DP 优化技巧——让算法飞起来
5.1 空间优化:滚动数组
int fib_space_optimized (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 c;
}
5.2 时间优化:斜率优化与四边形不等式 对于形如 dp[i] = min(dp[j] + cost(j, i)) 的转移,可以使用单调队列优化。
vector<int > maxSlidingWindow (vector<int >& nums, int k) {
if (nums.empty ()) return {};
deque<int > dq;
vector<int > result;
for (int i = 0 ; i < nums.size (); i++) {
if (!dq.empty () && dq.front () <= i - k) {
dq.pop_front ();
}
while (!dq.empty () && nums[dq.back ()] < nums[i]) {
dq.pop_back ();
}
dq.push_back (i);
if (i >= k - 1 ) {
result.push_back (nums[dq.front ()]);
}
}
return result;
}
5.3 状态压缩优化:位运算技巧
int setBit (int mask, int pos) {
return mask | (1 << pos);
}
int clearBit (int mask, int pos) {
return mask & ~(1 << pos);
}
bool isSet (int mask, int pos) {
return (mask >> pos) & 1 ;
}
int toggleBit (int mask, int pos) {
return mask ^ (1 << pos);
}
for (int sub = mask; sub > 0 ; sub = (sub - 1 ) & mask) {
}
第六章:DP 实战策略——解决陌生问题的通用方法
6.1 五步解题法
问题分析 :判断是否具有最优子结构和重叠子问题
状态定义 :dp[...] 表示什么?状态参数需要哪些?
状态转移 :如何从已知状态推出新状态?
边界条件 :最小子问题的答案是什么?
计算顺序 :应该按什么顺序计算状态?
6.2 调试技巧
void debugDP (vector<vector<int >>& dp) {
cout << "DP 表内容:" << endl;
for (int i = 0 ; i < dp.size (); i++) {
cout << "第" << i << "行: " ;
for (int j = 0 ; j < dp[i].size (); j++) {
cout << dp[i][j] << " " ;
}
cout << endl;
}
}
bool checkBoundary (int n, int m) {
if (n < 0 || m < 0 ) {
cout << "错误:访问了负索引" << endl;
return false ;
}
return true ;
}
6.3 常见错误与陷阱
数组越界 :转移时注意索引范围
初始化错误 :边界条件设置不当
状态转移遗漏 :没有考虑所有可能情况
循环顺序错误 :某些 DP 需要特定计算顺序
整数溢出 :使用 long long 或取模
第七章:从 DP 到人生——思维方式的升华
7.1 DP 思维的三个层次
第一层:暴力搜索 ——看到问题直接尝试所有可能
第二层:记忆化搜索 ——发现重复计算,开始记录答案
第三层:动态规划 ——系统性地自底向上构建解
7.2 生活中的 DP 思维 动态规划不仅是一种算法,更是一种解决问题的方法论 :
最优子结构 :大目标分解为小目标,每个阶段都做出最优选择
状态定义 :明确当前所处的位置和条件
状态转移 :知道如何从现在状态过渡到下一状态
边界条件 :清楚起点在哪里
无后效性 :过去的决定不影响未来的选择,着眼当下
7.3 给学习者的终极建议
从简单开始 :先掌握线性 DP、背包 DP 等基础模型
多画图 :在纸上画出状态转移图,直观理解
刻意练习 :每个类型刷 10-20 道经典题目
总结归纳 :建立自己的 DP 问题分类体系
融会贯通 :看到问题先想'这能不能用 DP 解?'
结语:DP 之美——复杂世界的简单规律 动态规划的美丽之处在于,它揭示了复杂问题背后的简单结构 。无论是斐波那契数列、最短路径还是旅行商问题,表面上千差万别,但核心都是:通过保存子问题的解,避免重复计算,最终得到整体最优解 。
学习 DP 的过程就像学习下棋:开始时只能看到眼前一步(暴力搜索),后来能看到两三步(记忆化搜索),最终能推演整个棋局(动态规划)。这种思维方式一旦掌握,不仅让你成为更好的程序员,更让你成为更优秀的思考者。
记住贝尔曼的名言:"动态规划是一种将复杂问题分解为更简单子问题的方法,通过解决每个子问题一次并将答案存储起来,避免重复解决相同的子问题。"
在算法世界中,动态规划是王冠上的明珠;在人生旅途中,它是指引方向的智慧之光。 开始你的 DP 之旅吧,从第一个 dp[0] = 1 开始,一步步构建属于你的最优解。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online