动态规划 线性 DP 五大经典模型:LIS、LCS、合唱队形、编辑距离 详解与模板

动态规划 线性 DP 五大经典模型:LIS、LCS、合唱队形、编辑距离 详解与模板

文章目录


经典线性 dp 问题有两个:最⻓上升⼦序列(简称:LIS)以及最⻓公共⼦序列(简称:LCS),这两道题⽬的很多⽅⾯都是可以作为经验,运⽤到别的题⽬中。⽐如:解题思路,定义状态表⽰的⽅式,推到状态转移⽅程的技巧等等。
因此,这两道经典问题是需要我们重点掌握的。

最长上升子序列

题目描述

在这里插入图片描述

题目解析
本题介绍最长上升子序列的一般解法,当数据量不大时用这种解法。
在此之前,小编先区分一下子数组和子序列,子数组需要是连续的,而子序列可以是间断的。

1、状态表示
dp[i]表示以i结尾的所有子序列中,最长的上升子序列。
2、状态转移方程
还是以最后一步推导状态转移方程,分两种情况:
一、当序列长度为1时,因为dp[i]自己本身就是一个子序列,所以dp[i]的最小取值可能是1(比如总序列是一个递减序列的话,那么每个dp[i]的取值都会是1)
二、当序列长度大于1时,如果第i个格子前面的所有格子中有比第i个格子小的格子,记为j,那么第i个格子就可以挂在j格子后面组成一个序列,序列长度就是dp[j]加1,因为我们要求最长上升子序列,所以需要遍历dp数组中从1到i-1的所有值,找出所有a[j]小于a[i]的格子,从中选出dp[j] + 1的最大值,该值为dp[i]的取值。

在这里插入图片描述


所以状态转移方程如下图所示:

在这里插入图片描述


3、初始化
因为我们没遍历到一个格子都会先把dp[i]置为1,所以dp数组不用初始化,用默认值0即可。
4、填表顺序
从左往右
5、输出结果
结果为dp数组从1到n的所有取值的最大值。

代码

#include<iostream>#include<algorithm>usingnamespace std;constint N =5010;int n;int a[N], dp[N];intmain(){//处理输入 cin >> n;for(int i =1; i <= n; i++){ cin >> a[i];}//初始化//按序填表int ret =0;for(int i =1; i <= n; i++){ dp[i]=1;for(int j =1; j < i; j++){//选出dp数组中1到i-1所有格子的最大值,把最大值加一赋给dp[i]if(a[j]< a[i]){ dp[i]=max(dp[i], dp[j]+1);}} ret =max(ret, dp[i]);} cout << ret << endl;return0;}

【模板】最长上升子序列

题目描述

在这里插入图片描述

题目解析
本题介绍最长上升子序列的推广解法,当数据量较大时用这种解法。
这里会利用贪心加二分优化时间复杂度,但是需要注意,这种方式是有局限性的,它只能求出最长上升子序列的长度,而不能还原出整个最长上升子序列。

在研究最长上升子序列时,其实只用关心序列的长度和序列最后一个元素是谁,我们并不关心序列具体长什么样子,本题是求解思路就是基于此实现的。
1、状态表示
对于原序列来说,我们先找出所有长度刚好是 i 的上升子序列(比如 i=3 时,就是所有长度为 3 的上升子序列),然后把这些子序列的最后一个数都列出来,在这些数里找最小的那个,这个最小的数就是 dp [i]。
这里用最小值充当dp[i]是用到了贪心的思想:要让序列尽可能长,就要让序列末尾元素尽可能小,让序列后面能挂更多元素。
2、状态转移方程
除了dp数组,我们还需要用一个全局变量len来存储最长上升子序列的长度。
填dp数组时我们要遍历整个a数组,首先处理边界情况,当len为0(序列为空)或者a[i]比dp[len]大(a[i]比最长序列的末尾元素还大)时,直接把len++,然后把a[i]赋值给dp[len]。否则就是一般情况,也就是序列本身已经有元素了,并且a[i]的值小于最长序列的末尾元素,这时我们要更新dp数组,但是len保持不变,更新策略是在dp数组中二分查找所有大于等于a[i]的元素中,其中的最小值所在的位置,然后把a[i]的值赋给该位置。(本质就是维护dp数组的特性:dp [i]为长度为i的所有子序列末尾元素的最小值)
3、初始化

4、填表顺序
从左往右
5、输出结果
len
代码

#include<iostream>usingnamespace std;constint N =1e5+10;int n;int a[N], dp[N];int len;//记录最长上升子序列长度intmain(){//处理输入 cin >> n;for(int i =1; i <= n; i++){ cin >> a[i];}//遍历原数组并填dp数组for(int i =1; i <= n; i++){if(len ==0|| a[i]> dp[len]){//边界情况,len和a[i]大于最长合法序列的末尾元素时//直接把a[i]放在dp数组++len处 dp[++len]= a[i];}//在dp数组中二分查找大于等于a[i]的最小值int l =1, r = len;while(l < r){int mid =(l + r)/2;if(dp[mid]>= a[i]){//答案在左半部分,包含mid r = mid;}else l = mid +1;}//把a[i]赋值给查找到的dp数组中大于等于a[i]的最小值 dp[l]= a[i];} cout << len;return0;}

合唱队形

题目描述

在这里插入图片描述

题目解析
本题是最长上升子序列问题的一个模板题,比较简单小编就简单说明一下。
依据题意,要找一个最长的先递增在递减的子序列,要让该子序列最长,就要分别让递增序列和递减序列都尽可能长,最终答案就是遍历所有同学找到一个同学i,以他为顶峰的递增序列+递减序列长度是最大的,但是需要注意同学i既在递增序列中也在递减序列中,相当于递增序列+递减序列把同学i算了两遍,所以最终答案要减一。
所以我们要对每一个同学求他的最长的递增子序列和最长递减的子序列,求最长递减的子序列也就相当于求从右往左的最长递增子序列,根据求最长的递增子序列的通用方法,我们知道dp[i]就表示以i结尾的所有子序列中,最长的上升子序列。本题我们要开两个dp数组,分别维护从左往右的最长递增子序列和从右往左的最长递增子序列。

代码

#include<iostream>#include<algorithm>usingnamespace std;constint N =110;//dp1维护从左往右的最长上升子序列//dp2维护从左往右的最长上升子序列int a[N], dp1[N], dp2[N];int n;intmain(){ cin >> n;for(int i =1; i <= n; i++){ cin >> a[i];}//初始化//按序填表//1.填dp1数组for(int i =1; i <= n; i++){ dp1[i]=1;for(int j =1; j < i; j++){if(a[j]< a[i]) dp1[i]=max(dp1[i], dp1[j]+1);}}//2.填dp2数组for(int i = n; i >=1; i--){ dp2[i]=1;for(int j = n; j > i; j--){if(a[j]< a[i]) dp2[i]=max(dp2[i], dp2[j]+1);}}//输出结果int ret =0;for(int i =1; i <= n; i++){//因为中间i的同学算了两遍,所以需要减一 ret =max(ret, dp1[i]+ dp2[i]-1);} cout << n - ret;return0;}

牛可乐和最长公共子序列

题目描述

在这里插入图片描述

题目解析
痛点:存储字符串问题!用string存储下标从0开始!
解决方案:
1、修改下标映射:i -> i - 1 j->j - 1
2、s = " " + s; + s;

本题小编不仅会讲题解,还会介绍一下如何分析两个数组的dp问题。
1、初始化
用一个二维数组dp[i][j],表示s字符串1到i区间内和t字符串1到j区间内,所有子序列中,最长公共子序列的长度。
2、状态转移方程
状态转移方程要分两种情况讨论:
1、当遍历到两个字符串相同的元素时(s[i] == t[j]),表示这个相同的元素的最长公共子序列的元素之一,要把这个这一对相同的元素选上,也就是把当前公共子序列长度在之前的基础上加1,那要如何找到之前的公共子序列长度呢?其实之前的公共子序列长度就是s字符串1到i - 1区间内和t字符串1到j区间内的最长公共子序列的长度,也就是dp[i - 1][j - 1],所以这种情况的状态转移方程:
dp[i][j] = dp[i - 1][j - 1] + 1
2、当遍历到两个字符串不同的元素时(s[i] != t[j]),一定不会把这一对不同的元素选到,这时为了填dp[i][j]就需要退而求其次,求在不看s[i]的情况下的最长公共子序列、在不看t[j]的情况下的最长公共子序列和求在不看s[i]并且不看t[j]的情况下的最长公共子序列,也就是分三种情况讨论:
在s数组的1到i区间和t数组的1到j - 1区间找最长公共子序列
在s数组的1到i - 1区间和t数组的1到j区间找最长公共子序列
在s数组的1到i - 1区间和t数组的1到j-1区间找最长公共子序列
dp[i][j]就是在这三种情况中取最值,但对于这种三种情况中取最值的逻辑,因为情况3是包含在情况1、2中的,所以情况3的方案一定小于等于情况1、2,所以在前两种情况中取最值即可。
dp[i][j] = max(dp[i][j - 1],dp[i - 1][j])

在这里插入图片描述


3、初始化
全部为0即可,因为本题的长度必定大于0,0不会干扰max取最大值的逻辑。
本题虽然是多组测试用例,但是填表是覆盖并且有顺序的,所以每组用例不用重置。
4、填表顺序
根据状态转移方程,填[i][j]时会用到左、上、和左上三个位置的格子,所以填表顺序是从上往下、从左往右。

在这里插入图片描述


5、输出结果
假设s字符串长度为n,t字符串长度为m:
dp[n][m]

代码

#include<iostream>#include<string>#include<algorithm>usingnamespace std;constint N =5010;int dp[N][N]; string s, t;intmain(){while(cin >> s >> t){int n = s.size();int m = t.size();//初始化//按序填表for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){if(s[i -1]== t[j -1]){ dp[i][j]= dp[i -1][j -1]+1;}else{ dp[i][j]=max(dp[i -1][j], dp[i][j -1]);}}}//输出结果 cout << dp[n][m]<< endl;}return0;}

编辑距离

题目描述

在这里插入图片描述

题目解析
1、状态表示
本题思路继续延续上一题的双数组dp问题,状态表示也类似:dp[i][j]表示把s字符串的1到i子字符串变成t字符串的1到j子字符串的最少操作次数。
2、状态转移方程
还是和上一题类似分两种情况讨论:
1、当遍历到两个字符串相同的元素时(s[i] == t[j]),说明此时不需要修改s数组最后一个元素,也就是对于dp[i][j]来说不需要将操作数加一,仅需要拿到把s字符串的1到i - 1子字符串变成t字符串的1到j - 1子字符串的最少操作次数即可,拿到的该最少操作次数为dp[i][j]的取值。
2、当遍历到两个字符串不同的元素时(s[i] != t[j]),说明此时需要修改s数组最后一个元素,因为题目给出了三种修改方式,所以这里我们要分三种情况讨论:
1.删除s字符串最后一个元素,然后把s字符串的1到i - 1子字符串变为t字符串,把s字符串的1到i - 1子字符串变为t字符串的最少操作次数在dp[i - 1][j]中,因为有删除操作,所以还需要加1。dp[i][j] = dp[i - 1][j] + 1

在这里插入图片描述

2、在s字符串最后面插入t字符串的最后一个元素,然后把s字符串的1到i字符串变为t字符串的1到j - 1子t子字符串。dp[i][j] = dp[i][j - 1] + 1

在这里插入图片描述

3、把s字符串最后一个元素改为t字符串最后一个元素,然后把s字符串的1到i - 1子字符串变为t字符串的1到j - 1子字符串。dp[i][j] = dp[i - 1][j - 1] + 1

在这里插入图片描述

3、初始化
本题初始化需要结合题目的实际意义,虽然题目数据范围的字符串长度从1开始,但是对于本题来说,研究字符串为空时还是有必要的,当把一个空串转换为一个长度为1的字符串时最少字符操作次数是1(插入1次),同理把一个空串转换为一个长度为2的字符串时最少字符操作次数是2,所以对于dp数组的边界情况dp[0][1]要初始化为1,dp[0][2]要初始化为2,依次类推。
当把长度为1的字符串转换为一个空串时最少字符操作次数是1(删除一次),同理当把长度为2的字符串转换为一个空串时最少字符操作次数是2,所以对于dp数组的边界情况dp[1][0]要初始化为1,dp[2][0]要初始化为2,依次类推。

在这里插入图片描述

4、填表顺序
从上往下,从左往后
5、输出结果
dp[n][m]

代码

#include<iostream>#include<string>#include<algorithm>usingnamespace std;constint N =2010;int dp[N][N]; string s, t;intmain(){ cin >> s >> t;int n = s.size();int m = t.size();//初始化for(int i =1; i <= n; i++){ dp[i][0]= i;}for(int i =1; i <= m; i++){ dp[0][i]= i;}//按序填表for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){if(s[i -1]== t[j -1]){ dp[i][j]= dp[i -1][j -1];}else{//删int a = dp[i -1][j]+1;//插int b = dp[i][j -1]+1;//改int c = dp[i -1][j -1]+1; dp[i][j]=min({ a, b, c });}}}//输出结果 cout << dp[n][m]<< endl;return0;}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

10分钟打造专属AI助手!ToDesk云电脑/顺网云/海马云操作DeepSeek哪家强?

10分钟打造专属AI助手!ToDesk云电脑/顺网云/海马云操作DeepSeek哪家强?

文章目录 * 一、引言 * 云计算平台概览 * ToDesk云电脑:随时随地用上高性能电脑 * 二 .云电脑初体验 * DeekSeek介绍 * 版本参数与特点 * 任务类型表现 * 1、ToDesk云电脑 * 2、顺网云电脑 * 3、海马云电脑 * 三、DeekSeek本地化实操和AIGC应用 * 1. ToDesk云电脑 * 2. 海马云电脑 * 3、顺网云电脑 * 四、结语 * 总结:云电脑如何选择? 一、引言 DeepSeek这些大模型让 AI 开发变得越来越有趣,但真要跑起来,可没那么简单! * 本地配置太麻烦:显卡不够、驱动难装、环境冲突,光是折腾这些就让人心态崩了。 * 云端性能参差不齐:选错云电脑,可能卡到爆、加载慢,还容易掉线,搞得效率直线下降。 * 成本难控:有的平台按小时计费,价格一会儿一个样,

By Ne0inhk
用 DeepSeek 打造你的超强代码助手

用 DeepSeek 打造你的超强代码助手

DeepSeek Engineer 是啥? 简单来说,DeepSeek Engineer 是一个基于命令行的智能助手。它能帮你完成这些事: * 快速读文件内容:比如你有个配置文件,直接用命令把它加载进助手,后续所有操作都可以基于这个文件。 * 自动改文件:它不仅能提建议,还可以直接生成差异表(diff),甚至自动应用修改。 * 智能代码生成:比如你让它生成代码片段,它会按照指定格式和规则直接返回。 更重要的是,这一切都是通过 DeepSeek 的强大 API 来实现的。想象一下,你有个贴身助手,不仅能听懂你的代码需求,还能直接动手帮你写! 核心功能拆解 我们先来看 DeepSeek Engineer 的几个核心能力,让你更好地理解它的强大之处。 1. 自动配置 DeepSeek 客户端 启动这个工具时,你只需要准备一个 .env 文件,里面写上你的 API Key,比如: DEEPSEEK_API_

By Ne0inhk
解锁DeepSeek潜能:Docker+Ollama打造本地大模型部署新范式

解锁DeepSeek潜能:Docker+Ollama打造本地大模型部署新范式

🐇明明跟你说过:个人主页 🏅个人专栏:《深度探秘:AI界的007》 🏅 🔖行路有良友,便是天堂🔖 目录 一、引言 1、什么是Docker 2、什么是Ollama 二、准备工作 1、操作系统 2、镜像准备 三、安装 1、安装Docker 2、启动Ollama 3、拉取Deepseek大模型 4、启动Deepseek  一、引言 1、什么是Docker Docker:就像一个“打包好的App” 想象一下,你写了一个很棒的程序,在自己的电脑上运行得很好。但当你把它发给别人,可能会遇到各种问题: * “这个软件需要 Python 3.8,但我只有 Python 3.6!

By Ne0inhk
深挖 DeepSeek 隐藏玩法·智能炼金术2.0版本

深挖 DeepSeek 隐藏玩法·智能炼金术2.0版本

前引:屏幕前的你还在AI智能搜索框这样搜索吗?“这道题怎么写”“苹果为什么红”“怎么不被发现翘课” ,。看到此篇文章的小伙伴们!请准备好你的思维魔杖,开启【霍格沃茨模式】,看我如何更新秘密的【知识炼金术】,我们一起来解锁更加刺激的剧情!友情提醒:《《《前方高能》》》 目录 在哪使用DeepSeek 如何对提需求  隐藏玩法总结 几个高阶提示词 职场打工人 自媒体创作 电商实战 程序员开挂 非适用场地 “服务器繁忙”如何解决 (1)硅基流动平台 (2)Chatbox + API集成方案 (3)各大云平台 搭建个人知识库 前置准备 下载安装AnythingLLM 选择DeepSeek作为AI提供商 创作工作区 导入文档 编辑  编辑 小编寄语 ——————————————————————————————————————————— 在哪使用DeepSeek 我们解锁剧情前,肯定要知道在哪用DeepSeek!咯,为了照顾一些萌新朋友,它的下载方式我放在下面了,拿走不谢!  (1)

By Ne0inhk