【C++】动态规划从入门到精通
一、动态规划基础概念详解
什么是动态规划
动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题,并存储子问题解以避免重复计算的优化算法。它适用于具有以下两个关键性质的问题:
最优子结构:问题的最优解包含子问题的最优解
重叠子问题:不同决策序列会重复求解相同的子问题
下面用一些例子(由浅入深)了解动态规划
1.1 斐波那契数列递归实现解析
int fib(int n) {if(n <= 1) return n; // 基准条件:F(0)=0, F(1)=1return fib(n-1) + fib(n-2); // 递归分解为两个子问题
}
代码解析:
- 递归终止条件:当n<=1时直接返回n值
- 递归关系:F(n) = F(n-1) + F(n-2)
- 问题分析:计算F(5)需要计算F(4)和F(3),而计算F(4)又需要F(3)和F(2),存在大量重复计算
- 时间复杂度:二叉树结构,O(2^n),空间复杂度O(n)(调用栈深度)
1.2 记忆化递归实现解析
int memo[100] = {0}; // 全局记忆数组,默认初始化为0int fib_memo(int n) {if(n <= 1) return n;if(memo[n] != 0) // 检查是否已计算过return memo[n];return memo[n] = fib_memo(n-1) + fib_memo(n-2); // 计算结果并存储
}
代码解析:
- memo数组存储已计算结果,初始值为0表示未计算
- 每次递归调用前检查是否已有缓存结果
- 通过空间换时间,将重复计算转化为查表操作
- 时间复杂度优化到O(n),空间复杂度O(n)
1.3 迭代法实现解析
int fib_tab(int n) {if(n == 0) return 0;int dp[n+1]; // 创建DP表dp[0] = 0; // 初始化基础条件dp[1] = 1;for(int i=2; i<=n; ++i)dp[i] = dp[i-1] + dp[i-2]; // 递推填充表格return dp[n];
}
代码解析:
- dp数组索引对应斐波那契数列的位置
- 初始化前两个已知值
- 循环从2开始逐步构建后续结果
- 时间复杂度O(n),空间复杂度O(n)(可优化为O(1))
二、经典问题深度解析
2.1 最长公共子序列(LCS)完整解析
问题描述:给定两个字符串text1和text2,返回它们的最长公共子序列的长度
int lcs(string text1, string text2) {int m = text1.size(), n = text2.size();// 创建(m+1)x(n+1)的二维DP表,+1是为了处理空字符串的情况vector<vector<int>> dp(m+1, vector<int>(n+1, 0));for(int i=1; i<=m; ++i) {for(int j=1; j<=n; ++j) {if(text1[i-1] == text2[j-1]) // 字符匹配(注意索引偏移)dp[i][j] = dp[i-1][j-1] + 1;else // 不匹配时取两个可能方向的最大值dp[i][j] = max(dp[i-1][j], dp[i][j-1]);}}return dp[m][n];
}
代码解析:
- 状态定义:
dp[i][j]表示text1前i个字符与text2前j个字符的LCS长度 - 初始化:第一行和第一列初始为0,表示空字符串的情况
- 状态转移:
- 当字符匹配时:LCS长度+1,继承左上方值+1
- 当字符不匹配时:取上方或左方的最大值
- 遍历顺序:双重循环按行填充表格
- 示例分析:
text1 = “abcde”, text2 = “ace”
DP表最终值为3(LCS为"ace")
2.2 0-1背包问题完整解析
问题描述:给定物品重量数组wt和价值数组val,背包容量W,求能装的最大价值
int knapsack(int W, vector<int>& wt, vector<int>& val) {int n = wt.size();vector<int> dp(W+1, 0); // 一维DP数组优化空间for(int i=0; i<n; ++i) { // 遍历每个物品for(int w=W; w>=wt[i]; --w) { // 逆序更新防止覆盖dp[w] = max(dp[w], // 不选当前物品dp[w - wt[i]] + val[i]); // 选择当前物品}}return dp[W];
}
代码解析:
- 状态定义:
dp[w]表示容量为w时的最大价值 - 空间优化:使用一维数组替代二维数组
- 逆序遍历原因:保证每个物品只被考虑一次,避免重复使用
- 状态转移方程分析:
- 不选物品i:价值保持dp[w]不变
- 选物品i:价值为dp[w-wt[i]] + val[i]
- 示例分析:
W=4, wt=[2,3,4], val=[3,4,5]
最终dp[4] = max(不选4: dp[4], 选4: dp[0]+5) = 5
2.3 编辑距离完整解析
问题描述:计算将word1转换成word2所需的最小操作次数(插入、删除、替换)
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; // 删除i次for(int j=0; j<=n; ++j) dp[0][j] = j; // 插入j次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] = 1 + min({dp[i-1][j], // 删除word1字符dp[i][j-1], // 插入word2字符dp[i-1][j-1]});// 替换字符}}}return dp[m][n];
}
代码解析:
- 状态定义:
dp[i][j]表示转换前i个字符到前j个字符的最小操作数 - 边界初始化:
- 第一列表示将word1删成空串的操作次数
- 第一行表示将空串插入成word2的操作次数
- 状态转移分析:
- 字符匹配:直接继承左上方值
- 字符不匹配:取三种操作的最小值+1
- 操作类型对应关系:
- 删除:相当于处理word1的前i-1个字符
- 插入:相当于处理word2的前j-1个字符
- 替换:相当于处理i-1和j-1的情况后修改字符
- 示例分析:
word1 = “horse”, word2 = “ros”
最终编辑距离为3(替换h→r,删除 r,删除 e)
三、动态规划优化技巧详解
3.1 斐波那契数列空间优化
int fib_opt(int n) {if(n == 0) return 0;int prev = 0, curr = 1; // 初始值F(0)=0, F(1)=1for(int i=2; i<=n; ++i) {int next = prev + curr; // 计算下一个值prev = curr; // 更新前一个值curr = next; // 更新当前值}return curr;
}
优化原理:
- 观察发现每个状态只依赖前两个状态
- 使用两个变量代替数组存储历史值
- 空间复杂度从O(n)降到O(1)
- 滚动更新机制:
- 每次迭代将prev更新为前一个curr
- curr更新为新的计算结果
3.2 背包问题空间优化
// 二维原始版本
int dp[n+1][W+1]; // 优化为一维数组
vector<int> dp(W+1, 0);
优化原理:
- 二维数组中每一行只依赖上一行的数据
- 逆序更新避免覆盖未使用的旧值
- 关键点:内层循环必须从W到wt[i]逆序进行
- 示例说明:
- 正序更新会导致物品被重复选取(完全背包问题)
- 逆序更新保证每个物品只被考虑一次
四、动态规划解题方法论
4.1 状态定义技巧
-
确定问题变量维度:
- 单序列问题(如LIS):一维状态dp[i]
- 双序列问题(如LCS):二维状态dp[i][j]
- 带约束问题(如背包):二维状态dp[i][w]
-
常见状态定义模式:
- “前i个元素…”:如dp[i]表示前i个元素的最优解
- “以第i个元素结尾…”:如最长递增子序列问题
- “位置(i,j)…”:如矩阵路径问题
4.2 状态转移方程建立
-
分析子问题关系:
- 如何从较小规模的子问题推导当前问题
- 举例:在编辑距离中,三种操作对应三种子问题转移路径
-
方程建立步骤:
(1) 列出所有可能的决策选项
(2) 计算每个决策对应的子问题解
(3) 选择最优决策并组合结果
4.3 初始化技巧
-
边界条件处理:
- 空字符串/空集合的处理
- 初始值的物理意义(如背包容量为0时价值为0)
-
特殊值初始化示例:
// 矩阵路径问题初始化第一行和第一列 for(int i=0; i<m; ++i) dp[i][0] = 1; for(int j=0; j<n; ++j) dp[0][j] = 1;
五、综合案例分析
5.1 最大子数组和
问题描述:求整数数组中和最大的连续子数组
int maxSubArray(vector<int>& nums) {int currMax = nums[0], globalMax = nums[0];for(int i=1; i<nums.size(); ++i) {// 决策:继续扩展子数组 or 重新开始currMax = max(nums[i], currMax + nums[i]);// 更新全局最大值globalMax = max(globalMax, currMax);}return globalMax;
}
算法解析:
- 状态定义:currMax表示以当前元素结尾的最大子数组和
- 状态转移方程:
currMax = max(nums[i], currMax + nums[i]) - 空间优化:仅需维护两个变量
- 示例分析:
输入:[-2,1,-3,4,-1,2,1,-5,4]
输出:6(子数组[4,-1,2,1])
5.2 不同路径问题
问题描述:m x n网格从左上角到右下角的唯一路径数
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];
}
算法解析:
- 状态定义:dp[i][j]表示到达(i,j)的路径数
- 状态转移方程:dp[i][j] = 上方路径数 + 左方路径数
- 初始化技巧:第一行和第一列都只有1种路径
- 空间优化:可用一维数组替代,dp[j] += dp[j-1]
六、动态规划调试技巧
6.1 DP表可视化
- 打印DP表中间状态
// 在LCS代码中插入调试输出 for(auto& row : dp) {for(int val : row) cout << val << " ";cout << endl; } - 观察表数据是否符合预期
6.2 边界测试用例
- 空输入测试:空字符串、空数组等
- 极值测试:n=0, W=0等特殊情况
- 示例测试:使用题目给出的示例验证
6.3 常见错误排查
- 数组越界:检查索引是否正确(特别是从1开始的情况)
- 初始化错误:验证边界条件是否正确设置
- 循环顺序错误:检查是否按正确依赖顺序填充表格
- 状态转移方程错误:用简单用例手动验证
七、动态规划复杂度分析指南
7.1 时间复杂度计算
-
基本公式:状态数 × 每个状态的转移成本
- LCS问题:O(mn)状态 × O(1)转移 = O(mn)
- 背包问题:O(nW)状态 × O(1)转移 = O(nW)
-
多项式时间与伪多项式时间:
- 背包问题的O(nW)称为伪多项式时间
- 当W很大时(如指数级),算法效率会显著下降
7.2 空间复杂度优化
-
滚动数组技巧:
- 二维→一维:当当前行只依赖前一行时
- 示例:斐波那契数列、背包问题
-
状态压缩技巧:
- 使用位运算表示状态集合
- 常见于旅行商问题(TSP)等状压DP
八、动态规划进阶路线图
8.1 学习路径建议
-
基础阶段(1-2周):
- 斐波那契数列
- 爬楼梯问题
- 最大子数组和
-
提高阶段(2-4周):
- 背包问题系列
- 字符串编辑问题
- 矩阵路径问题
-
精通阶段(1-2月):
- 树形DP(二叉树最大路径和)
- 状态压缩DP(TSP问题)
- 区间DP(矩阵链乘法)
8.2 推荐练习题目
| 题目类型 | LeetCode题号 | 难度 |
|---|---|---|
| 爬楼梯 | 70 | 简单 |
| 最长递增子序列 | 300 | 中等 |
| 零钱兑换 | 322 | 中等 |
| 正则表达式匹配 | 10 | 困难 |
| 买卖股票最佳时机 | 121/123 | 中等 |
九、动态规划代码模板库
9.1 一维DP模板
int dp[n];
dp[0] = initial_value;for(int i=1; i<n; ++i) {dp[i] = compute(dp[...]);
}return dp[n-1];
9.2 二维DP模板
vector<vector<int>> dp(m, vector<int>(n, 0));// 初始化边界
for(int i=0; i<m; ++i) dp[i][0] = ...;
for(int j=0; j<n; ++j) dp[0][j] = ...;// 填充表格
for(int i=1; i<m; ++i) {for(int j=1; j<n; ++j) {dp[i][j] = compute(dp[i-1][j], dp[i][j-1], ...);}
}
十、动态规划常见问题FAQ
Q:如何判断一个问题是否可以用DP解决?
A:检查问题是否具有:
- 最优子结构性质
- 重叠子问题性质
- 无后效性(当前决策不影响之前状态)
Q:DP和分治法的区别是什么?
A:分治法将问题分解为独立的子问题,而DP处理的是重叠的子问题
Q:如何处理环形结构问题?
A:常用技巧:
- 破环成链(复制数组)
- 分类讨论(考虑包含首元素和不包含的情况)
Q:如何选择记忆化递归还是迭代法?
A:
- 记忆化递归更直观,适合树形结构问题
- 迭代法效率更高,适合需要空间优化的情况
- 动态规划导图

相关文章:
【C++】动态规划从入门到精通
一、动态规划基础概念详解 什么是动态规划 动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题,并存储子问题解以避免重复计算的优化算法。它适用于具有以下两个关键性质的问题: 最优子结构&…...
OpenCV计算摄影学(23)艺术化风格化处理函数stylization()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 风格化的目的是生成不以照片写实为目标的多种多样数字图像效果。边缘感知滤波器是风格化处理的理想选择,因为它们能够弱化低对比度区…...
S32K144外设实验(三):ADC单通道连续采样(中断)
这次的实验比较简单,主要目的就是验证一下ADC的中断功能,思路是使用软件触发ADC的连续单通道采样,将采样值通过串口发送到上位机观察数是否正确。 其实官方并不推荐使用中断的方式,这种方式会占用大量的CPU资源,笔者安…...
LeetCode 第19~21题
LeetCode 第19题:删除链表的倒数第N个结点 题目描述 给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。 难度:中等 题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode) 示例…...
Web3 时代数据保护的关键挑战与应对策略
Web3 时代数据保护的关键挑战与应对策略 随着互联网技术的飞速发展,我们正步入 Web3 时代,这是一个以去中心化、用户主权和数据隐私为核心的新时代。在这个时代,数据保护成为了一个至关重要的议题。本文将探讨 Web3 时代数据保护面临的主要挑…...
为什么labelme框选图片后闪退
Labelme 软件框选图片后闪退的解决方案 Labelme 是一种常用的图像标注工具,但在实际使用过程中可能会遇到一些问题,比如框选图片后程序突然闪退。以下是针对该问题的具体分析和解决方法: 可能原因及对应解决措施 标签文件异常 如果某些图片…...
C# I/O 核心用法
在 C# 中,输入输出(I/O)操作是处理文件、目录、流等数据交互的核心功能。本文将从基础到高级,系统讲解 C# 中文件 I/O 的实现方式、最佳实践及常见场景解决方案。 一、核心类与命名空间 1、System.IO 命名空间,…...
SpringBoot之如何集成SpringDoc最详细文档
文章目录 一、概念解释1、OpenAPI2、Swagger3、Springfox4、Springdoc5. 关系与区别 二、SpringDoc基本使用1、导包2、正常编写代码,不需要任何注解3、运行后访问下面的链接即可 三、SpringDoc进阶使用1、配置文档信息2、配置文档分组3、springdoc的配置参数**1. 基…...
Oracle 数据迁移至 GaussDB 注意事项
将数据从 Oracle 迁移到 GaussDB(华为分布式数据库)时,需充分考虑架构差异、语法兼容性、数据一致性等核心问题。以下是关键注意事项及操作建议: 一、迁移前的准备工作 兼容性评估 语法差异:Oracle 使用 PL/SQL&#x…...
【智能体】| 知识库、RAG概念区分以及智能体是什么
文章目录 前言简介大模型“幻觉”问题如何解决“幻觉”问题? RAG、智能体、RAG智能体概念什么是检索增强型生成(RAG)模拟简单的RAG场景 AI系统中的智能体是什么什么是Agentic RAG?Agentic RAG如何工作?Agentic RAG架构…...
二分查找的应用
什么时候用二分查找? 数据具有二段性的时候 第一题: 题解代码: class Solution { public:int search(vector<int>& nums, int target) {int left 0,right nums.size()-1;while(left<right){int mid left (right-left)/2;//中…...
Android Compose 框架基础按钮模块深度剖析(四)
Android Compose 框架基础按钮模块深度剖析 一、引言 在现代 Android 应用开发中,Android Compose 框架以其声明式编程范式和简洁高效的开发体验,逐渐成为开发者构建用户界面的首选。而注解在 Android Compose 框架中扮演着至关重要的角色,…...
redis搭建一主一从+keepalived(虚拟IP)实现高可用
redis搭建一主一从keepalived(虚拟IP)实现高可用 前提 有两台机器:如 10.50.3.141 10.50.3.142,虚拟ip如:10.50.3.170 安装redis(两台机器执行): # 启用Remi仓库(CentOS 7) sudo yum install…...
【Function】Azure Function通过托管身份或访问令牌连接Azure SQL数据库
【Function】Azure Function通过托管身份或访问令牌连接Azure SQL数据库 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 【Function】Azure Function通过托管身份或访问令牌连接Azu…...
MySQL日志全解析:类型、用途与运维实践
引言 MySQL作为最流行的关系型数据库之一,其日志系统是运维人员理解数据库状态、排查问题、保证数据安全的核心工具。不同类型的日志记录了数据库活动、错误信息、数据变更等关键内容。本文将深入解析MySQL各类日志的作用、配置参数及运维注意事项,帮助…...
《算法笔记》9.2小节——数据结构专题(2)->二叉树的遍历 问题 A: 复原二叉树(同问题 C: 二叉树遍历)
题目描述 小明在做数据结构的作业,其中一题是给你一棵二叉树的前序遍历和中序遍历结果,要求你写出这棵二叉树的后序遍历结果。 输入 输入包含多组测试数据。每组输入包含两个字符串,分别表示二叉树的前序遍历和中序遍历结果。每个字符串由…...
小程序开发中的用户反馈收集与分析
我们在开发小程序的过程中根据开发过程中的代码及业务场景,以下是针对需求管理系统的用户反馈收集与分析方案设计: 需求管理系统用户反馈收集与分析方案 一、反馈数据模型设计 // 新增Feedback模型(app/admin/model/Feedback.php) namespace app\admin\model; use think\…...
Flink 通过 Chunjun Oracle LogMiner 实时读取 Oracle 变更日志并写入 Doris 的方案
文章目录 一、 技术背景二、 关键技术1、 Oracle LogMiner2、 Chunjun 的 LogMiner 关键流程3、修复 Chunjun Oracle LogMiner 问题 一、 技术背景 在大数据实时同步场景中,需要将 Oracle 数据库的变更数据(CDC) 采集并写入 Apache Doris&am…...
WordPress系统获取webshell的攻略
一.后台修改模板拿WebShell 1.进入Vulhub靶场并执⾏以下命令开启靶场;在浏览器中访问并安装好 #执⾏命令 cd /vulhub/wordpress/pwnscriptum docker-compose up -d 2. 修改其WP的模板,登陆WP后点击 【外 观】 --》 【编辑】 --》 404.php 3.插入一句话木…...
JMeter基本介绍
Apache JMeter 工具详解 一、JMeter 简介 JMeter 是 Apache 基金会开源的 Java 应用程序,主要用于 性能测试、负载测试 和 功能测试。它通过对服务器或网络资源模拟多种负载条件(如并发用户、持续压力),帮助评估系统性能指标&am…...
npm 安装 pnpm 的详细步骤及注意事项
一、安装步骤 1.全局安装 pnpm npm install -g pnpm2.验证安装 pnpm -v输出版本号即表示安装成功。 二、升级 pnpm 若已安装旧版本,可通过以下命令升级: npm install -g pnpmlatest三、配置镜像加速 设置淘宝镜像 pnpm config set registry http…...
蓝桥杯2023年第十四届省赛真题-子矩阵
题目来自DOTCPP: 暴力思路(两个测试点超时): 题目要求我们求出子矩阵的最大值和最小值的乘积,我们可以枚举矩阵中的所有点,以这个点为其子矩阵的左上顶点,然后判断一下能不能构成子矩阵。如果可…...
如何在 Node.js 中使用 .env 文件管理环境变量 ?
Node.js 应用程序通常依赖于环境变量来管理敏感信息或配置设置。.env 文件已经成为一种流行的本地管理这些变量的方法,而无需在代码存储库中公开它们。本文将探讨 .env 文件为什么重要,以及如何在 Node.js 应用程序中有效的使用它。 为什么使用 .env 文…...
Redis BitMap 用户签到
Redis Bitmap Bitmap(位图)是 Redis 提供的一种用于处理二进制位(bit)的特殊数据结构,它基于 String 类型,每个 bit 代表一个布尔值(0 或 1),可以用于存储大规模的二值状…...
未来办公与生活的新范式——智慧园区
在信息化与智能化技术飞速发展的今天,智慧园区作为一种新兴的城市发展形态,正逐步成为推动产业升级、提升城市管理效率、改善居民生活质量的重要力量。智慧园区不仅融合了先进的信息技术,还深刻体现了可持续发展的理念,为园区内的…...
Hugging Face预训练GPT微调ChatGPT(微调入门!新手友好!)
Hugging Face预训练GPT微调ChatGPT(微调入门!新手友好!) 在实战中,⼤多数情况下都不需要从0开始训练模型,⽽是使⽤“⼤⼚”或者其他研究者开源的已经训练好的⼤模型。 在各种⼤模型开源库中,最…...
【CSS3】化神篇
目录 平面转换平移旋转改变旋转原点多重转换缩放倾斜 渐变线性渐变径向渐变 空间转换平移视距旋转立体呈现缩放 动画使现步骤animation 复合属性animation 属性拆分逐帧动画多组动画 平面转换 作用:为元素添加动态效果,一般与过渡配合使用 概念&#x…...
Unity音频混合器如何暴露参数
音频混合器是Unity推荐管理音效混音的工具,那么如何使用代码对它进行管理呢? 首先我在AudioMixer的Master组中创建了BGM和SFX的分组,你也可以直接用Master没有问题。 这里我以BGM为例,如果要在代码中进行使用就需要将参数暴露出去…...
Vue keepalive学习用法
在Vue中,<keep-alive>的include属性用于指定需要缓存的组件,其实现方式如下: 1. 基本用法 • 字符串形式:通过逗号分隔组件名称,匹配到的组件会被缓存。 <keep-alive include"ComponentA,ComponentB&…...
5-1 使用ECharts将MySQL数据库中的数据可视化
方法一:使用Python Flask框架搭建API 对于技术小白来说,使用ECharts将MySQL数据库中的数据可视化需要分步骤完成。以下是详细的实现流程: 一、技术架构 后端服务:使用Python Flask框架搭建API(简单易学ÿ…...
