【算法】——一键解决动态规划

前言

动态规划是一种高效解决​​重叠子问题​​和​​最优子结构​​问题的算法思想。它通过​​分治+记忆化​​,将复杂问题分解为子问题,并存储中间结果,避免重复计算,从而大幅提升效率。

​为什么重要?

  1. ​优化暴力解法​​:如斐波那契数列,递归复杂度为O(2n),而动态规划可优化至O(n)。
  2. ​解决经典难题​​:如背包问题、最短路径、编辑距离等,动态规划往往是​​最优解法​​。
  3. ​广泛应用​​:从算法竞赛到实际开发(如资源调度、股票交易策略),动态规划都是核心工具之一。

掌握动态规划,能让你在算法设计与优化中事半功倍!

动态规划流程

我个人是觉得动态规划是相当难的,因为我不太擅长找规律

动态规划就像是我们熟知的找规律,通过已知项得出一个规律

再用这个规律,套用到已知项上,得出未知项

虽然难,但动态规划也有自己的模板,让你看到一个动态规划有个思考方向

动态规划流程

  1. 创建dp表,确定状态表示
  2. 确定状态转移方程
  3. 初始化
  4. 顺序填充
  5. 确定返回值

现在,分别介绍一下

创建dp表,并确定状态表示

​DP表(动态规划表)​​是动态规划算法的核心工具。

它本质是一个数组,其中的每一个元素都是一个解

但这个解的意义是未知的,需要我们自己去规定,即解的状态表示

直接地

例如:Fn = Fn-1 + Fn-2

  • 我们创建一个dp表,此时dp[i]表示的值就是Fi的值

间接地

例如:求字符串中一个连续的无重复元素子串的最大长度

  • 我们创建一个dp表,此时dp[i]表示的值就是以i位置为结尾的无重复元素子串的最大长度

确定状态转移方程

如何从已知的dp[i]得到未知的dp[i],就需要得出状态转移方程

这就是找规律,也是动态规划最难的一部分,得出正确的状态转移方程,是解决问题的关键部分

有些状态转移方程是明着告诉你的,有些则需要自己去找

这一步相当考验你的经验,解决这一步的唯一方法:多练多思考多画图

顺序填充

dp表的数据元素代表解,我们求解问题,就是求出指定dp[i]

用状态转移方程求出dp[i],可能需要dp[i-1]、dp[i-2]等一个或者多个

有了前面,才有后面,因此必须顺序填充

例如:Fn=Fn-1+Fn-2

  • 我们求一个dp[i],就需要先知道dp[i-1]和dp[i-2]的长度

初始化

进行顺序填充前,需要先做好准备工作,防止顺序填充的时候遇到错误

例如:Fn = Fn-1+Fn-2

当你填充dp[1]的时候,你就会发现根本没有所谓的F1-2和F1-1,这就是越界错误

我们需要用初始化来避免越界错误

确定返回值

具体情况具体分析,根据题目要求确定返回值

例如:得出第几项的值

  • 这时候直接返回dp[i]即可

再者:求字符串中一个连续的无重复元素子串的最大长度

  • 这时候就需要返回最大的dp[i]

动态规划题型

斐波那契数列模型

斐波那契数列就是求出第几个斐波那契数的值

这是最简单的一类动态规划题型,明明白白地告诉你怎么创建dp表,怎么进行状态表示

属于直接明牌了

流程解决:

  1. 创建dp表:创建一个大小为n+1的dp表,dp[i]表示的值即为F(i)
  2. 确定状态转移方程:F(n) = F(n-1) + F(n-2)
  3. 初始化:dp[0]=0,dp[1]=1
  4. 顺序填充:从左往右依次填充
  5. 确定返回值:dp[n]表示的值就是F(n)的值,返回dp[n]即可

代码如下

class Solution {
public:int fib(int n) {if(n==0) return 0;if(n==1) return 1;//创建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];}
};

路径问题

求出到达目标地点有多少种方式

这种就是需要自己来找规律了

经典例题:不同路径

 

流程解决:

创建dp表,确定状态表示

  • 有多少个格子,dp表就需要多大,即dp表的大小就是m*n
  • 状态表示有个技巧,题目最后要什么,你就表示什么,要求返回抵达最后一个位置的所有路径总数,则dp[i][j]代表的就是这个抵达这个位置的所有路径总数

确定状态转移方程

这里的状态转移方程就没有明示了,需要我们自己去找

但也不难到达一个格子只有两种方式,从正上方的格子下来,从左方的格子过来

而到达当前格子的正上方格子有多种方式,到达左方格子也有多种方式

因此,到达当前格子总路径数 = 到达上方格子的路径数 + 到达右方格子的路径数

状态转移方程:dp[i][j] = dp[i][j-1] + dp[i-1][j]

初始化

  • dp[0][0]等于1

顺序填充

  • 从左往右,从上往下进行填充
  • 填充的时候,需要注意左方格子和上方格子是否存在,进行取舍

确定返回值

  • 返回最后一个格子对应的dp[i][j]即可

代码:

class Solution {
public:int uniquePaths(int m, int n) {//创建dp表vector<vector<int>> dp(m,vector<int>(n,0));//初始化dp[0][0]=1;for(int i=0;i<m;i++){for(int j=0;j<n;j++){if(i==0 && j!=0){dp[i][j]+=dp[i][j-1];}if(j==0 && i!=0){dp[i][j]+=dp[i-1][j];}else if(i!=0 && j!=0){dp[i][j]=dp[i-1][j]+dp[i][j-1];}}}return dp[m-1][n-1];}
};

简单多状态

在常规动态规划问题中,每个子问题通常只需一个状态表示(如dp[i])。但在​​多状态DP​​中,每个步骤需要维护​​多个并行的状态​​,通过它们之间的关系推导最终解。

​典型特征​​:

  • 问题在每个步骤有​​多种可能的状态​​(如"持有/未持有股票"、"偷/不偷当前房屋")
  • 需要为​​每种状态单独建立DP表​​或状态变量
  • 状态之间存在​​相互转移关系​

这是我个人问题有点难度的题型,因为你需要考虑多种状态下的状态表示

经典例题:打家劫舍

流程解决:

创建dp表

  • 创建dp表,dp表的大小即为给定房屋的个数,即为n
  • 状态表示:dp[i]表示偷到当前房屋时的最大金额数

但此时房屋可能被偷,也可能没有被偷!

  • 被偷时,该房屋的最大金额数应该加上当前房屋的金额
  • 没有被偷时,该房屋的最大金额数则不应该加上当前房屋的金额

而一张dp表,是无法表示偷、不偷两种状态下的值的

因此,需要两种dp表

  • dpf表,dpf[i]表示当前房屋被偷后,所获得的最大金额
  • dpg表,dpg[i]表示当前房屋没有被偷时,所获得的最大金额

确定状态转移方程

当前房屋被偷

  • 相邻的房屋一定没有被偷

dpf状态转移方程:dpf[i] = dpg[i] +nums[i];

当前房屋没有被偷,相邻的房屋可能被偷,也可能没有被偷

  • 上一个房屋没有被偷时,状态转移方程:dpg[i] = dpg[i-1];
  • 上一个房屋被偷时,状态转移方程:dpg[i] = dpf[i-1]

dpg[i]表示当前房屋没有被偷时的最大金额,因此取两者最大即可

dpg状态转移方程:dpg[i] = max(dpg[i-1],dpf[i-1])

初始化

从开始位置开始

  • 偷,dpf[0] = nums[0]
  • 没偷,dpg[0] = 0;

顺序填充

  • 从左到右,依次填充两个dp表

确定返回值

  • 返回最后一个房屋的最大金额即可,即max(dpf[n-1],dpg[n-1])

代码:

class Solution {
public:int rob(vector<int>& nums) {if(nums.size()==0) return 0;//创建dp表int n = nums.size();vector<int> f(n);auto g = f;//初始化f[0]=nums[0];g[0]=0;//顺序填充for(int i = 1;i<n;i++){f[i]=g[i-1]+nums[i];g[i]=max(g[i-1],f[i-1]);}return max(f[n-1],g[n-1]);}
};

子数组问题

子数组问题是动态规划的经典应用场景,通常涉及​​连续子数组​​的最优解(如最大和、最长长度等)

​子数组问题的DP特点

  • ​连续性​​:子数组要求元素连续,与子序列(可不连续)不同
  • ​单串DP​​:通常用dp[i]表示​​以第i个元素结尾的子数组的解​
  • ​状态转移​​:要么延续前一个状态,要么从当前元素重新开始

经典例题:最大子数组和

流程解决:

创建dp表,确定状态表示

  • 创建一个大小和数组大小一样的dp表
  • 状态表示:dp[i]表示以i位置为结尾的子数组的最大和

确定状态转移方程

连续的子数组,所有下标必须连续,不能间断

  • dp[i] = dp[i-1] + nums[i]

子数组可以是多个,也可以是一个,即从当前下标开始

  • dp[i] = nums[i]

dp[i]记录的是以i位置为结尾的子数组的最大和,取两者中的最大值

状态转移方程:dp[i] = max(dp[i-1]+nums[i],nums[i])

初始化

  • dp[0] = nums[0]

顺序填充

  • 从左向右,依次对dp表进行填充

确定返回值

  • 我们需要的是该数组的最大子数组和,并不是最后一个位置的最大子数组和
  • 所以我们需要找到dp表中的最大值,并返回

代码:

class Solution {
public:int maxSubArray(vector<int>& nums) {if(nums.size()==0) return 0;if(nums.size()==1) return nums[0];//创建dp表:dp[i]表示当前位置的最大连续子数组和int n = nums.size();vector<int> dp(n);//初始化dp[0]=nums[0];int ret = nums[0];//顺序填充for(int i=1;i<n;i++){dp[i]=max(nums[i],dp[i-1]+nums[i]);if(dp[i]>ret) ret = dp[i];}return ret;}
};

子序列问题

子序列问题是动态规划中的另一大类经典问题,与子数组问题最大的区别在于​​元素不需要连续​​。

经典例题:最长递增子序列

流程解决:

创建dp表,确定状态表示

  • 创建一个和数组大小一样的dp表
  • 状态表示:dp[i]的值表示以i位置为结尾的递增子序列的最大长度

确定状态转移方程

  • 递增子序列可以有多个元素,这是元素可以连续,也可以不连续
  • dp[i] = dp[j] + 1;

注意:这里的dp[j]可能并不与dp[i]相邻,可以相邻,也可以不相邻,前提是满足nums[j]<nums[i]

  • 递增子序列也可能只有一个元素,即当前元素,代表之前没有比其小的元素
  • dp[i] = 1

而dp[i]表示的是以i位置为结尾的递增子序列的最大长度

很多人可能会觉得需要在这两者中取最大值

但并不是,这里只能选择符合要求的值

一旦前面没有比当前元素小的元素,那么递增子序列只能重头开始,即长度为1

而如果有,则就需要再在原来的基础长度上加1即可

所以,我们可以在创建dp表的时候,就将所有的dp[i]设置为1

后续如果nums[i]的前面有更小值,直接更新即可!

初始化

  • 不需要初始化

顺序填充

  • 从左往右依次填充dp表

确定返回值

  • 返回dp表中的最大的dp[i]

回文串问题​

回文串问题是动态规划的经典应用场景,通常涉及​​子串/子序列的回文性质判断​​和​​最值计算​​。以下是系统性解题框架和典型例题分析。

​回文串问题的DP特点​

  • ​对称性​​:需判断字符串的对称性质(如s[i]==s[j]
  • ​中心扩展​​:多数问题可转化为​​区间DP​​(从短子串向长子串递推)
  • ​状态定义​​:通常用dp[i][j]表示​​子串s[i..j]的回文性质​

经典例题:回文子串

思路:

将给定字符串的所有子串进行枚举,并对其每个进行判断是否是回文串

这里的枚举并不是真正的枚举,而是进行一种映射

如:使用[i,j],表示一段区间

流程解决:

创建dp表,确定状态表示

  1. 根据给定的字符串大小n,创建一个n*n的dp表
  2. 状态表示:dp[i,j]:表示s中区间为[i,j]的子串是回文字符串

例如:s="abcter" 

dp[0][2] 表示"abc"是否是回文字符串

确定状态转移方程

判断 s 的区间[i,j]是否是回文子串

如果 s[i] == s[j] , 则分三种情况进行判断 

  1. i == j,[i,j]代表一个字符,则dp[i][j]=true
  2. i+1 == j,[i,j]代表两个相邻的字符,则dp[i,j]=true
  3. i+1 <j,代表不相邻的两个字符相同,接下来看看dp[i+1][j-1]是否是回文字符串

所以,状态转移方程:dp[i][j] = i+1<j?dp[i+1][j-1]:true;

初始化

  • 将dp表中的所有元素置为false

顺序填充

  • 这里的顺序填充不在是我们正常思维的顺序,而是倒序
  • 在状态转移方程中,我们可以发现,求dp[i][j]可能需要dp[i+1][j-1],而dp[i+1][j-1]在dp[i][j]的左下方
  • 因此填充顺序是从下到上,从左到右

确定返回值

  1. 首先设置一个计数器
  2. 遍历一遍dp表,遇到true就让计数器+1
  3. 最后返回计数器的值即可

结语

  动态规划是一种将复杂问题分解为相互关联的子问题的算法思想,其核心在于利用最优子结构和避免重复计算来提升效率。我们通过定义状态、建立转移方程、初始化边界条件和确定计算顺序这四个关键步骤,可以系统性地解决各类动态规划问题。无论是求最值、处理序列问题,还是解决背包或状态机问题,动态规划都展现出强大的建模能力。记住,许多看似复杂的问题,往往都能通过寻找子问题之间的递推关系来优雅解决。动态规划的魅力就在于它用空间换时间的智慧,让原本可能指数级复杂度的问题变得可解。

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

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

相关文章

uniApp开发微信小程序-连接蓝牙连接打印机上岸!

历经波折三次成功上岸&#xff01; 三次经历简单絮叨一下&#xff1a;使用uniAppvue开发的微信小程序&#xff0c;使用蓝牙连接打印机&#xff0c;蓝牙所有的接口都是插件中封装的&#xff0c;用的插件市场中的这个&#xff1a; dothan-lpapi-ble &#xff1b;所以&#xff0c…

软件系统安全设计方案,信息化安全建设方案(Word原件)

1.1 总体设计 1.1.1 设计原则 1.2 物理层安全 1.2.1 机房建设安全 1.2.2 电气安全特性 1.2.3 设备安全 1.2.4 介质安全措施 1.3 网络层安全 1.3.1 网络结构安全 1.3.2 划分子网络 1.3.3 异常流量管理 1.3.4 网络安全审计 1.3.5 网络访问控制 1.3.6 完…

wsl2+ubuntu22.04安装blenderproc教程

本章教程,介绍如何在windows操作系统上通过wsl2+Ubuntu22.04上安装blenderproc。 一、pipi安装方式 推荐使用minconda3安装Python环境。 pip install Blenderproc二、源码安装 1、下载源码 git clone https://github.com/DLR-RM/BlenderProc2、安装依赖 cd BlenderProc &am…

Blender 转 STL 文件全攻略:从基础到进阶

在 3D 建模与打印领域&#xff0c;Blender 凭借其强大的功能和开源特性&#xff0c;深受创作者喜爱。而 STL 文件格式&#xff0c;作为 3D 打印行业的通用标准&#xff0c;能被绝大多数 3D 打印软件和设备所识别。因此&#xff0c;将 Blender 模型转换为 STL 文件&#xff0c;是…

Ansys Electronics 变压器 ACT

你好&#xff0c; 在本博客中&#xff0c;我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤&#xff0c;该变压器为同心组件&#xff0c;双绕组&#xff0c;采用正弦电压激励&#xff0c;并应用…

nacos配置达梦数据库驱动源代码步骤

1.在父工程pom.xml添加依赖&#xff1a; <dependency><groupId>com.dameng</groupId><artifactId>DmJdbcDriver18</artifactId><version>8.1.1.193</version> </dependency> 2.在nacos-config模块pom.xml添加依赖&#xff1…

4.9-4.10学习总结 Stream流练习+方法引用+异常

Stream流练习&#xff1a; 1.打印数组内的偶数。 import java.util.*; import java.util.function.BiConsumer; public class test {public static void main(String[] args) {ArrayList<Integer> listnew ArrayList<>();Collections.addAll(list,1,2,3,4,5,6,7,…

FPGA系统开发板调试过程不同芯片的移植步骤介绍

目录 1.我目前使用的开发板 2.不同开发板的移植 步骤一&#xff1a;芯片型号设置 步骤二&#xff1a;约束修改 步骤三、IP核更新 关于FPGA系统开发板调试过程中不同芯片的移植。我需要先理清楚FPGA开发中移植到不同芯片的一般流程。首先&#xff0c;移植通常涉及到更换FPG…

复现QGIS-MCP教程

由于Claude国内下载不了尝试使用Cursor 下载安装Cursor Cursor - The AI Code Editor 本示例安装的是0.46版本 UV安装 简介 安装 安装成功 配置环境变量 验证 下载代码 git clone gitgithub.com:jjsantos01/qgis_mcp.git QGIS插件安装 文件拷贝 您需要将 qgis_mcp_plu…

java笔记03

基本数据类型 数据值是存储在自己的空间中。 特点&#xff1a;赋值给其他变量&#xff0c;也是赋的真实的值。 引用数据类型 数据值是存储在其他空间中&#xff0c;自己空间中存储的是地址值。 特点&#xff1a;赋值给其他变量&#xff0c;赋的地址值。 综合练习 使用 ctrl…

【开发工具】快速自定义图标元素的颜色

如果你想要一个轻量级、简单易用 的小工具来快速自定义图标元素的颜色&#xff08;比如调整 SVG/PNG 图标的颜色&#xff0c;或者生成多色图标&#xff09;&#xff0c;可以试试以下工具&#xff1a; 1. 在线工具&#xff08;无需安装&#xff09; SVG/PNG 图标改色 - Recol…

【CompletableFuture】异步编程

CompletableFuture异步编程 CompletableFuture介绍与传统 Future 的对比使用方法1. 使用 supplyAsync&#xff08;有返回值&#xff09;使用 runAsync&#xff08;无返回值&#xff09;指定自定义线程池 处理异步结果1. thenApply&#xff1a;转换结果2.thenAccept&#xff1a;…

【TS学习】(23)理解类的双重角色

在 TypeScript 中&#xff0c;类&#xff08;class&#xff09;不仅是一个运行时的值&#xff08;即可以实例化对象的构造函数&#xff09;&#xff0c;同时也是一个类型声明。具体来说&#xff0c;类在 TypeScript 中既声明了值&#xff0c;也声明了类型&#xff0c;并且它的类…

IAP Firmware Upload Tools.exe IAP 网络固件升级教程

IAP是In Application Programming的简写&#xff0c;IAP升级可以被视为固件升级的一种形式,它是一种在应用程序运行过程中对固件进行更新的技术手段。允许MCU在运行过程中对MCU User Flash的部分区域进行烧写,目的是为了代替编程器对MCU烧录的依赖。 主程序UI 软件按钮说明&a…

Uniapp当中的async/await的作用

一、原始代码的行为&#xff08;使用 async/await&#xff09; const getUserMessagePlan async () > {// 等待两个异步操作完成const tabsList await message.getTagesList(); // 等待获取标签列表const tagsStateList await message.getTagsStateList(); // 等…

设计模式 Day 5:夯实观察者模式(Boost 实战精讲)

今天我们继续深入观察者模式的学习&#xff0c;不再局限于手写的抽象结构&#xff0c;而是聚焦于真实项目中如何使用成熟框架&#xff08;如 Boost.Signals2&#xff09;高效落地观察者模式。 本篇采用**“理论解析 问答讲解 实战用例”**结构&#xff0c;帮助你从设计思想到…

设计模式 Day 3:抽象工厂模式(Abstract Factory Pattern)详解

经过前两天的学习&#xff0c;我们已经掌握了单例模式与工厂方法模式&#xff0c;理解了如何控制实例个数与如何通过子类封装对象的创建逻辑。 今天&#xff0c;我们将进一步深入“工厂”体系&#xff0c;学习抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;&a…

MySQL:事务的理解

一、CURD不加控制&#xff0c;会有什么问题 &#xff08;1&#xff09;因为&#xff0c;MySQL里面存的是数据&#xff0c;所以很有可能会被多个客户访问&#xff0c;所以mysqld可能一次会接受到多个关于CURD的请求。&#xff08;2&#xff09;且mysql内部是采用多线程来完成数…

蓝桥杯刷题--宝石组合

在一个神秘的森林里&#xff0c;住着一个小精灵名叫小蓝。有一天&#xff0c;他偶然发现了一个隐藏在树洞里的宝藏&#xff0c;里面装满了闪烁着美丽光芒的宝石。这些宝石都有着不同的颜色和形状&#xff0c;但最引人注目的是它们各自独特的 “闪亮度” 属性。每颗宝石都有一个…

DAY06:【pytorch】图像增强

1、基本概念 数据增强&#xff0c;又称数据增广、数据扩增&#xff0c;是对训练集进行变换&#xff0c;使训练集更丰富&#xff0c;从而让模型更具泛化能力 2、裁剪 — — Crop 2.1 transforms.CenterCrop 功能&#xff1a;从图像中心裁剪图片 size&#xff1a;所需裁剪图…