【C++】动态规划从入门到精通

news2025/3/21 21:38:27

一、动态规划基础概念详解

什么是动态规划
动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题,并存储子问题解以避免重复计算的优化算法。它适用于具有以下两个关键性质的问题:

最优子结构:问题的最优解包含子问题的最优解

重叠子问题:不同决策序列会重复求解相同的子问题

下面用一些例子(由浅入深)了解动态规划

1.1 斐波那契数列递归实现解析

int fib(int n) {
    if(n <= 1) return n;          // 基准条件:F(0)=0, F(1)=1
    return fib(n-1) + fib(n-2);  // 递归分解为两个子问题
}

代码解析

  1. 递归终止条件:当n<=1时直接返回n值
  2. 递归关系:F(n) = F(n-1) + F(n-2)
  3. 问题分析:计算F(5)需要计算F(4)和F(3),而计算F(4)又需要F(3)和F(2),存在大量重复计算
  4. 时间复杂度:二叉树结构,O(2^n),空间复杂度O(n)(调用栈深度)

1.2 记忆化递归实现解析

int memo[100] = {0};  // 全局记忆数组,默认初始化为0

int 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);  // 计算结果并存储
}

代码解析

  1. memo数组存储已计算结果,初始值为0表示未计算
  2. 每次递归调用前检查是否已有缓存结果
  3. 通过空间换时间,将重复计算转化为查表操作
  4. 时间复杂度优化到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];
}

代码解析

  1. dp数组索引对应斐波那契数列的位置
  2. 初始化前两个已知值
  3. 循环从2开始逐步构建后续结果
  4. 时间复杂度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];
}

代码解析

  1. 状态定义:dp[i][j]表示text1前i个字符与text2前j个字符的LCS长度
  2. 初始化:第一行和第一列初始为0,表示空字符串的情况
  3. 状态转移:
    • 当字符匹配时:LCS长度+1,继承左上方值+1
    • 当字符不匹配时:取上方或左方的最大值
  4. 遍历顺序:双重循环按行填充表格
  5. 示例分析:
    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];
}

代码解析

  1. 状态定义:dp[w]表示容量为w时的最大价值
  2. 空间优化:使用一维数组替代二维数组
  3. 逆序遍历原因:保证每个物品只被考虑一次,避免重复使用
  4. 状态转移方程分析:
    • 不选物品i:价值保持dp[w]不变
    • 选物品i:价值为dp[w-wt[i]] + val[i]
  5. 示例分析:
    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];
}

代码解析

  1. 状态定义:dp[i][j]表示转换前i个字符到前j个字符的最小操作数
  2. 边界初始化:
    • 第一列表示将word1删成空串的操作次数
    • 第一行表示将空串插入成word2的操作次数
  3. 状态转移分析:
    • 字符匹配:直接继承左上方值
    • 字符不匹配:取三种操作的最小值+1
  4. 操作类型对应关系:
    • 删除:相当于处理word1的前i-1个字符
    • 插入:相当于处理word2的前j-1个字符
    • 替换:相当于处理i-1和j-1的情况后修改字符
  5. 示例分析:
    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)=1
    for(int i=2; i<=n; ++i) {
        int next = prev + curr;  // 计算下一个值
        prev = curr;  // 更新前一个值
        curr = next;  // 更新当前值
    }
    return curr;
}

优化原理

  1. 观察发现每个状态只依赖前两个状态
  2. 使用两个变量代替数组存储历史值
  3. 空间复杂度从O(n)降到O(1)
  4. 滚动更新机制:
    • 每次迭代将prev更新为前一个curr
    • curr更新为新的计算结果

3.2 背包问题空间优化

// 二维原始版本
int dp[n+1][W+1]; 

// 优化为一维数组
vector<int> dp(W+1, 0);

优化原理

  1. 二维数组中每一行只依赖上一行的数据
  2. 逆序更新避免覆盖未使用的旧值
  3. 关键点:内层循环必须从W到wt[i]逆序进行
  4. 示例说明:
    • 正序更新会导致物品被重复选取(完全背包问题)
    • 逆序更新保证每个物品只被考虑一次

四、动态规划解题方法论

4.1 状态定义技巧

  1. 确定问题变量维度:

    • 单序列问题(如LIS):一维状态dp[i]
    • 双序列问题(如LCS):二维状态dp[i][j]
    • 带约束问题(如背包):二维状态dp[i][w]
  2. 常见状态定义模式:

    • “前i个元素…”:如dp[i]表示前i个元素的最优解
    • “以第i个元素结尾…”:如最长递增子序列问题
    • “位置(i,j)…”:如矩阵路径问题

4.2 状态转移方程建立

  1. 分析子问题关系:

    • 如何从较小规模的子问题推导当前问题
    • 举例:在编辑距离中,三种操作对应三种子问题转移路径
  2. 方程建立步骤:
    (1) 列出所有可能的决策选项
    (2) 计算每个决策对应的子问题解
    (3) 选择最优决策并组合结果

4.3 初始化技巧

  1. 边界条件处理:

    • 空字符串/空集合的处理
    • 初始值的物理意义(如背包容量为0时价值为0)
  2. 特殊值初始化示例:

    // 矩阵路径问题初始化第一行和第一列
    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;
}

算法解析

  1. 状态定义:currMax表示以当前元素结尾的最大子数组和
  2. 状态转移方程:
    currMax = max(nums[i], currMax + nums[i])
  3. 空间优化:仅需维护两个变量
  4. 示例分析:
    输入:[-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];
}

算法解析

  1. 状态定义:dp[i][j]表示到达(i,j)的路径数
  2. 状态转移方程:dp[i][j] = 上方路径数 + 左方路径数
  3. 初始化技巧:第一行和第一列都只有1种路径
  4. 空间优化:可用一维数组替代,dp[j] += dp[j-1]

六、动态规划调试技巧

6.1 DP表可视化

  1. 打印DP表中间状态
    // 在LCS代码中插入调试输出
    for(auto& row : dp) {
        for(int val : row) cout << val << " ";
        cout << endl;
    }
    
  2. 观察表数据是否符合预期

6.2 边界测试用例

  1. 空输入测试:空字符串、空数组等
  2. 极值测试:n=0, W=0等特殊情况
  3. 示例测试:使用题目给出的示例验证

6.3 常见错误排查

  1. 数组越界:检查索引是否正确(特别是从1开始的情况)
  2. 初始化错误:验证边界条件是否正确设置
  3. 循环顺序错误:检查是否按正确依赖顺序填充表格
  4. 状态转移方程错误:用简单用例手动验证

七、动态规划复杂度分析指南

7.1 时间复杂度计算

  1. 基本公式:状态数 × 每个状态的转移成本

    • LCS问题:O(mn)状态 × O(1)转移 = O(mn)
    • 背包问题:O(nW)状态 × O(1)转移 = O(nW)
  2. 多项式时间与伪多项式时间:

    • 背包问题的O(nW)称为伪多项式时间
    • 当W很大时(如指数级),算法效率会显著下降

7.2 空间复杂度优化

  1. 滚动数组技巧:

    • 二维→一维:当当前行只依赖前一行时
    • 示例:斐波那契数列、背包问题
  2. 状态压缩技巧:

    • 使用位运算表示状态集合
    • 常见于旅行商问题(TSP)等状压DP

八、动态规划进阶路线图

8.1 学习路径建议

  1. 基础阶段(1-2周):

    • 斐波那契数列
    • 爬楼梯问题
    • 最大子数组和
  2. 提高阶段(2-4周):

    • 背包问题系列
    • 字符串编辑问题
    • 矩阵路径问题
  3. 精通阶段(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:检查问题是否具有:

  1. 最优子结构性质
  2. 重叠子问题性质
  3. 无后效性(当前决策不影响之前状态)

Q:DP和分治法的区别是什么?
A:分治法将问题分解为独立的子问题,而DP处理的是重叠的子问题

Q:如何处理环形结构问题?
A:常用技巧:

  1. 破环成链(复制数组)
  2. 分类讨论(考虑包含首元素和不包含的情况)

Q:如何选择记忆化递归还是迭代法?
A:

  • 记忆化递归更直观,适合树形结构问题
  • 迭代法效率更高,适合需要空间优化的情况
  • 动态规划导图
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2319181.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV计算摄影学(23)艺术化风格化处理函数stylization()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 风格化的目的是生成不以照片写实为目标的多种多样数字图像效果。边缘感知滤波器是风格化处理的理想选择&#xff0c;因为它们能够弱化低对比度区…

S32K144外设实验(三):ADC单通道连续采样(中断)

这次的实验比较简单&#xff0c;主要目的就是验证一下ADC的中断功能&#xff0c;思路是使用软件触发ADC的连续单通道采样&#xff0c;将采样值通过串口发送到上位机观察数是否正确。 其实官方并不推荐使用中断的方式&#xff0c;这种方式会占用大量的CPU资源&#xff0c;笔者安…

Web3 时代数据保护的关键挑战与应对策略

Web3 时代数据保护的关键挑战与应对策略 随着互联网技术的飞速发展&#xff0c;我们正步入 Web3 时代&#xff0c;这是一个以去中心化、用户主权和数据隐私为核心的新时代。在这个时代&#xff0c;数据保护成为了一个至关重要的议题。本文将探讨 Web3 时代数据保护面临的主要挑…

SpringBoot之如何集成SpringDoc最详细文档

文章目录 一、概念解释1、OpenAPI2、Swagger3、Springfox4、Springdoc5. 关系与区别 二、SpringDoc基本使用1、导包2、正常编写代码&#xff0c;不需要任何注解3、运行后访问下面的链接即可 三、SpringDoc进阶使用1、配置文档信息2、配置文档分组3、springdoc的配置参数**1. 基…

【智能体】| 知识库、RAG概念区分以及智能体是什么

文章目录 前言简介大模型“幻觉”问题如何解决“幻觉”问题&#xff1f; RAG、智能体、RAG智能体概念什么是检索增强型生成&#xff08;RAG&#xff09;模拟简单的RAG场景 AI系统中的智能体是什么什么是Agentic RAG&#xff1f;Agentic RAG如何工作&#xff1f;Agentic RAG架构…

二分查找的应用

什么时候用二分查找&#xff1f; 数据具有二段性的时候 第一题&#xff1a; 题解代码&#xff1a; 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;//中…

【Function】Azure Function通过托管身份或访问令牌连接Azure SQL数据库

【Function】Azure Function通过托管身份或访问令牌连接Azure SQL数据库 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 【Function】Azure Function通过托管身份或访问令牌连接Azu…

Flink 通过 Chunjun Oracle LogMiner 实时读取 Oracle 变更日志并写入 Doris 的方案

文章目录 一、 技术背景二、 关键技术1、 Oracle LogMiner2、 Chunjun 的 LogMiner 关键流程3、修复 Chunjun Oracle LogMiner 问题 一、 技术背景 在大数据实时同步场景中&#xff0c;需要将 Oracle 数据库的变更数据&#xff08;CDC&#xff09; 采集并写入 Apache Doris&am…

WordPress系统获取webshell的攻略

一.后台修改模板拿WebShell 1.进入Vulhub靶场并执⾏以下命令开启靶场&#xff1b;在浏览器中访问并安装好 #执⾏命令 cd /vulhub/wordpress/pwnscriptum docker-compose up -d 2. 修改其WP的模板&#xff0c;登陆WP后点击 【外 观】 --》 【编辑】 --》 404.php 3.插入一句话木…

蓝桥杯2023年第十四届省赛真题-子矩阵

题目来自DOTCPP&#xff1a; 暴力思路&#xff08;两个测试点超时&#xff09;&#xff1a; 题目要求我们求出子矩阵的最大值和最小值的乘积&#xff0c;我们可以枚举矩阵中的所有点&#xff0c;以这个点为其子矩阵的左上顶点&#xff0c;然后判断一下能不能构成子矩阵。如果可…

如何在 Node.js 中使用 .env 文件管理环境变量 ?

Node.js 应用程序通常依赖于环境变量来管理敏感信息或配置设置。.env 文件已经成为一种流行的本地管理这些变量的方法&#xff0c;而无需在代码存储库中公开它们。本文将探讨 .env 文件为什么重要&#xff0c;以及如何在 Node.js 应用程序中有效的使用它。 为什么使用 .env 文…

Redis BitMap 用户签到

Redis Bitmap Bitmap&#xff08;位图&#xff09;是 Redis 提供的一种用于处理二进制位&#xff08;bit&#xff09;的特殊数据结构&#xff0c;它基于 String 类型&#xff0c;每个 bit 代表一个布尔值&#xff08;0 或 1&#xff09;&#xff0c;可以用于存储大规模的二值状…

未来办公与生活的新范式——智慧园区

在信息化与智能化技术飞速发展的今天&#xff0c;智慧园区作为一种新兴的城市发展形态&#xff0c;正逐步成为推动产业升级、提升城市管理效率、改善居民生活质量的重要力量。智慧园区不仅融合了先进的信息技术&#xff0c;还深刻体现了可持续发展的理念&#xff0c;为园区内的…

Hugging Face预训练GPT微调ChatGPT(微调入门!新手友好!)

Hugging Face预训练GPT微调ChatGPT&#xff08;微调入门&#xff01;新手友好&#xff01;&#xff09; 在实战中&#xff0c;⼤多数情况下都不需要从0开始训练模型&#xff0c;⽽是使⽤“⼤⼚”或者其他研究者开源的已经训练好的⼤模型。 在各种⼤模型开源库中&#xff0c;最…

【CSS3】化神篇

目录 平面转换平移旋转改变旋转原点多重转换缩放倾斜 渐变线性渐变径向渐变 空间转换平移视距旋转立体呈现缩放 动画使现步骤animation 复合属性animation 属性拆分逐帧动画多组动画 平面转换 作用&#xff1a;为元素添加动态效果&#xff0c;一般与过渡配合使用 概念&#x…

Unity音频混合器如何暴露参数

音频混合器是Unity推荐管理音效混音的工具&#xff0c;那么如何使用代码对它进行管理呢&#xff1f; 首先我在AudioMixer的Master组中创建了BGM和SFX的分组&#xff0c;你也可以直接用Master没有问题。 这里我以BGM为例&#xff0c;如果要在代码中进行使用就需要将参数暴露出去…

如何理解分布式光纤传感器?

关键词&#xff1a;OFDR、分布式光纤传感、光纤传感器 分布式光纤传感器是近年来备受关注的前沿技术&#xff0c;其核心在于将光纤本身作为传感介质和信号传输介质&#xff0c;通过解析光信号在光纤中的散射效应&#xff0c;实现对温度、应变、振动等物理量的连续、无盲区、高…

PMP-项目运行环境

你好&#xff01;我是 Lydia-穎穎 ♥感谢你的陪伴与支持 ~~~ 欢迎一起探索未知的知识和未来&#xff0c;现在lets go go go!!! 1. 影响项目的要素 项目存在在不同的环境下&#xff0c;环境对于项目的交付产生不同的影响。需了解环境对于项目的影响&#xff0c;采取相应措施应对…

shell 脚本搭建apache

#!/bin/bash # Set Apache version to install ## author: yuan# 检查外网连接 echo "检查外网连接..." ping www.baidu.com -c 3 > /dev/null 2>&1 if [ $? -eq 0 ]; thenecho "外网通讯良好&#xff01;" elseecho "网络连接失败&#x…

Huawei 鲲鹏(ARM/Aarch64)服务器安装KVM虚拟机(非桌面视图)

提出问题 因需要进行ARM架构适配&#xff0c;需要在Huawei Taishan 200k&#xff08;CPU&#xff1a; Kunpeng 920 5231K&#xff09;上&#xff0c;创建几台虚拟机做为开发测试环境。 无奈好久没搞了&#xff0c;看了一下自己多年前写的文章&#xff1a;Huawei 鲲鹏&#xf…