C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性

1. 前言

动态规划处理字符相关案例中,求最长公共子序列以及求最短编辑距离,算是经典中的经典案例。

讲解此类问题的算法在网上一抓应用一大把,即便如此,还是忍不住有写此文的想法。毕竟理解、看懂都不算是真正掌握,唯有瞧出其中玄机,能有自己独有的见解和不一样的感悟方算是把知识学到灵魂深入。

好了!闲话少说,进入正题。

2. 最长公共子序列(LCS)

2.1 问题描述

最长公共子序列,指找出 2 个或多个字符串中的最长公共子序列。

如字符串 s1=kabcs2=taijc,其最长公共子序列是ac

Tips: 子序列只要求其中字符保持和原字符串中一样的顺序,而不一定连续。

2.2 递归思想

这是一道求最值的题目,只要是求最值,必然会存在多个选择,原理很简单,如果没有多个选择,还有必要纠结谁是最大谁是最小吗?

Tips: 在你面前有苹果、桔子、香蕉……你只能选择一个,这时候方有纠结。如果面前只有苹果,还会纠结吗?

面对此问题,可以采用化整为零的思想,从宏观层面转移到微观层面,缩小问题的规模的递归思想。

如为字符串s1设置位置指针 i,为字符串s2设置位置指针j,则问题可以抽象为如下函数。函数的语义:ij作为起始位置时字符串s1,s2的最长公共子序列。

int lcs(string s1,int i,string s2,int j);
//如果 s1、s2为全局变量,函数可以是
int lcs(int i,int j);  

41.png

  • 初始时,i=0j=0意味求解完整的s1s2的最长公共子序列。此时规模最大,无法直接得到答案。如此,把问题延续到规模较小的子问题。

42.png

​ 上文说过,求最值一定存在多个选择的,原始问题中的k!=t,则可存在如下 3 种选择:

​ A、i不动,j+1。即把i指向作为起始位置的s1字符串和j+1作为起始位置的s2字符串继续比较。可算为一个子问题。

43.png

​ B、j不动,i+1。即把i+1指向作为起始位置的s1字符串和j作为起始位置的s2字符串继续比较。可算为另一个子问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cr2f8B0w-1691975983175)(D:\红泥巴\我的课程体系\数据结构与算法\动态规划系列\images\44.png)]

​ C、ij同时移动到下一个位置。即把i+1指向作为起始位置的s1字符串和j+1作为起始位置的s2字符串继续比较。也算为一个子问题。

45.png

​ 也就是说,当原始问题中ij指向位置字符不相同时,存在 3 个选择。至于子问题如何求解,这个归功于递归思想。

Tips: 递归最大的好处就是只需要确定基础函数的功能,然后确定子问题,则子问题的内部如何求解站在宏观角度可以不管。反之它可以一步一步继续缩小问题规模,直到有答案为止。

​ 然后在3 种选择中,返回值最大的那一个作为当前的问题的结果。

int lcs(string s1,int i,string s2,int j){if(s1[i]!=s2[j]){//有 3 种选择int sel_1=lcs(s1,i,s2,j+1);int sel_2=lcs(s1,i+1,s2,j);int sel_3=lcs(s1,i+1,s2,j+1);return max(sel_1,sel_2,sel_3);} 
}
  • 如下图所示,当i和j所指向位置的值相同时,必然在当前子问题中就找到了一个公共字符,则最终结果就是后续子问题的结果基础上加 1 ,则为最长公共子序列为原来的值加 1

    Tips: 在海滩上捡贝壳时,当前拾到了一个,回家时最终能拾到的贝壳一定是当前拾到的这一个加上后续所拾到的贝壳。

45.png

​ 同时移动 ij,进入规模较小的子问题。如下图所示。

​ 此时可总结一下,使用递归求最长公共子序列,类似于玩消消乐,相同,则消掉,直接进入剩下的内容。不相同,选择会多些。

46.png

int lcs(string s1,int i,string s2,int j){if(s1[i]!=s2[j]){//有 3 种选择int sel_1=lcs(s1,i,s2,j+1);int sel_2=lcs(s1,i+1,s2,j);int sel_3=lcs(s1,i+1,s2,j+1);//三者之中选择最大返回值}else{//只有一个选择return lcs(s1,i+1,s2,j+1)+1;}
}
  • 递归边界。当i==s1.size() 或 j==s2.size()时,说明已经扫描到了子符串的最后。如下图所示,无论哪一个指针先到达字符串的末尾,则都不再存在任何公共子序列。

47.png

int lcs(string s1,int i,string s2,int j){if(i==s1.size() || j==s2.size())return 0;if(s1[i]!=s2[j]){//有 3 种选择int sel_1=lcs(s1,i,s2,j+1);int sel_2=lcs(s1,i+1,s2,j);int sel_3=lcs(s1,i+1,s2,j+1);//三者之中选择最大返回值}else{//只有一个选择return lcs(s1,i+1,s2,j+1)+1;}
}

上述是基于递归的角度分析问题,完整的代码如下:

#include <iostream>
using namespace std;
int lcs(string s1,int i,string s2,int j) {if(i==s1.size() || j==s2.size())return 0;if(s1[i]!=s2[j]) {//有 3 种选择int sel_1=lcs(s1,i,s2,j+1);int sel_2=lcs(s1,i+1,s2,j);int sel_3=lcs(s1,i+1,s2,j+1);int maxVal=max(sel_1,sel_2);maxVal=max(maxVal,sel_3);return maxVal;} else {//只有一个选择return lcs(s1,i+1,s2,j+1)+1;}
}
int main() {string s1,s2;cin>>s1>>s2;int res= lcs(s1,0,s2,0);cout<<res;return 0;
}

当字符串的长度较大时,基于递归的运算量会较大,问题在于递归算法中存在大量的重叠子问题。

2.3 重叠子问题

绘制递归树,可清晰看到重叠子问题的存在。

48.png

并且可以看到 sel_1sel_2分支包括sel_3分支,可以使用缓存方案解决递归中的重叠子问题,让重叠子问题只被计算一次。完整代码如下 :

#include <iostream>
#include <map>
using namespace std;
//缓存
map<pair<int,int>,int> cache;
int lcs(string s1,int i,string s2,int j) {if(i==s1.size() || j==s2.size())return 0;pair<int,int> p= {i,j};if (cache[p] ) {return cache[p];}if(s1[i]!=s2[j]) {//有 3 种选择int sel_1=lcs(s1,i,s2,j+1);int sel_2=lcs(s1,i+1,s2,j);cache[p]=max(sel_1,sel_2);;} else {//只有一个选择cache[p]=lcs(s1,i+1,s2,j+1)+1;}return 	cache[p];
}
int main() {string s1,s2;cin>>s1>>s2;int res= lcs(s1,0,s2,0);cout<<res;return 0;
}

递归实现性能不可观,代码层面也稍显繁琐。类似于这样求最值的问题,可以试着使用动态规划来实现。

2.4 动态规划

递归解决问题的思想是由上向下,所谓由上向下,指先搁置规模较大的问题,等规模较小的子问题解决后再回溯出大问题的解。通过上文贴的递归树可以清晰看到求解流程。

动态规划的思想是由下向上,是基于枚举思想。记录每一个子问题的解,最终推导出比之更大问题的解。当然,要求小问题具有独立性和最优性。

无论由上向下,还是由下向上,其本质都是知道子问题答案后,再求解出大问题的答案。动态规划算法是直接了当,递归是迂回求解。

现以求字符串的最长公共子序列为例,讲解动态规划的求解过程。

构建dp数组,用来记录所有子问题的解,类似于递归实现的缓存器。 于本问题而言,dp是一个二维数组,理论上讲,从A推导出B,再从B推导出C……问题域关心的是最后的推导结论C,之前使用过的历史推导结论其实是可以不用存储。有点类似于"忘恩负义",所以可以对于dp数组进行压缩。

  • 构建dp二维数组。先初始化数组的第一行和第一列的值为0。推导必须有一个源头,这里的 0就是源头。

    s1=""、s2="a……" 或当s1="a……"、s2=""或当s1=""、s2=""时可认为最长公共子序列的值为0

49.png

  • 如图,让i=1、j=1,比较 s1[i]和s2[j]位置的字符,显然kt是不相等的。递归是看后面(还没求解)有多少个子问题可以选择,动态规划是看前面(已经求解)有多个子问题会影响当前子问题。对于当前位置而言,对之有影响的位置有3个。如下图标记为黄色区域位置。

    1位置坐标为(i,j-1)。表示s1中有ks2中无t时最长公共子序列的值。

    2位置坐标为(i-1,j-1)。表示s1中无ks2中无t时最长公共子序列的值。

    3位置坐标为(i-1,j)。表示s1中无ks2中有t时最长公共子序列的值。

50.png

​ 可以舍弃位置3,然后在位置1和位置2中求最大值。

51.png

  • i=1不变,改成j的值。一路比较s1[i]s2[j]中值,因都不相等,根据前面的分析,很容易填写出dp值。

52.png

  • 移动i=2,重置j=1且移动j

    ij所在位置的字符不相等时的问题已经分析。

    如下图,当 i=2,j=2时,s[i]和s[j]的值相等,则影响此位置值的前置位置应该是哪个?

54.png

​ 相等,显然最长公共子序列会增加1,问题是在哪一个前置子问题的值上加 1

​ 其实,只需要在如下黄色区域位置的值上加上1,此位置表示当s1和s2中都没有a的时候。

56.png

  • 按如上分析原理,可以把整个dp表填写完成。

58.png

编码实现:

#include <iostream>
#include <map>
using namespace std;
int dp[100][100]= {0};
void lcs(string s1,string s2) {//初始化动态规划表for(int i=0; i<s2.size(); i++)dp[0][i]=0;for(int i=0; i<s1.size(); i++)dp[i][0]=0;for(int i=1; i<=s1.size(); i++) {for(int j=1; j<=s2.size(); j++)if(s1[i-1]==s2[j-1]) {//相等dp[i][j]=dp[i-1][j-1]+1;} else {dp[i][j]=max(dp[i-1][j],dp[i][j-1]);}}
}
int main() {string s1,s2;cin>>s1>>s2;lcs(s1,s2);for(int i=0; i<=s1.size(); i++) {for(int j=0; j<=s2.size(); j++) {cout<<dp[i][j]<<"\t";}cout<<endl;}cout<<"最长公共子序列:"<<endl;int res=dp[s1.size()][s2.size()];cout<<res<<endl;return 0;
}

测试结果:

59.png

4. 总结

最长公共子序列很有代表性,分析基于递归和动态规划的实现过程,可以帮助我们理解此类问题,且解决此类问题。

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

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

相关文章

浅谈统一权限管理服务的设计与开发

作者 | 天地练心 导读 本文详细探讨了统一权限管理服务&#xff08;MPS&#xff09;的设计与开发&#xff0c;针对企业内部多平台权限管理混乱的问题&#xff0c;提出了一套综合RBAC、ACL、DAC权限模型的解决方案。文章从需求分析、技术选型、功能设计等方面全面介绍了MPS的构建…

阿里云ACP知识点

前言&#xff1a;记录ACP错题 1、在创建阿里云ECS时&#xff0c;每台服务器必须要包含_______用来存储操作系统和核心配置。 系统盘&#xff08;不是实例&#xff0c;实例是一个虚拟的计算环境&#xff0c;由CPU、内存、系统盘和运行的操作系统组成&#xff1b;ESC实例作为云…

2023国赛数学建模E题思路分析

文章目录 0 赛题思路1 竞赛信息2 竞赛时间3 建模常见问题类型3.1 分类问题3.2 优化问题3.3 预测问题3.4 评价问题 4 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 竞赛信息 全国大学生数学建模…

纯js点击按钮切换首页部分页面

像我这种大数据的&#xff0c;不会前端的&#xff0c;懒得学框架&#xff0c;现在有gpt了&#xff0c;前端对于我来说&#xff0c;用原生的更加友好&#xff0c;毕竟算法gpt都能优化。 首页我有个页面&#xff0c;然后我现在想点击gm替换上面的统计&#xff0c;点击用户替换回…

Flask Web开发实战(狼书)| 笔记第1、2章

前言 2023-8-11 以前对网站开发萌生了想法&#xff0c;又有些急于求成&#xff0c;在B站照着视频敲了一个基于flask的博客系统。但对于程序的代码难免有些囫囵吞枣&#xff0c;存在许多模糊或不太理解的地方&#xff0c;只会照葫芦画瓢。 而当自己想开发一个什么网站的时&…

SpringCloud微服务之间如何进行用户信息传递(涉及:Gateway、OpenFeign组件)

目录 1、想达到的效果2、用户信息在微服务之间传递的两种途径3、用RuoYi-Cloud为例进行演示说明&#xff08;1&#xff09;网关将用户信息写在请求头中&#xff08;2&#xff09;业务微服务之间通过OpenFeign进行调用&#xff0c;并且将用户信息写在OpenFeign准备的请求头中&am…

Qt+C++自定义控件仪表盘动画仿真

程序示例精选 QtC自定义控件仪表盘动画仿真 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<QtC自定义控件仪表盘动画仿真>>编写代码&#xff0c;代码整洁&#xff0c;规则&…

浅谈SMT行业MES系统生产管理的特点

一、SMT生产车间在电子制造中起重要作用的部分&#xff0c;主要具备以下生产特点&#xff1a; 1.高密度和高速度&#xff1a; SMT生产车间中的电子元器件一般来说较为精小&#xff0c;且被紧密地排列在PCB上。这就要求SMT生产车间的机械设备具备高精度和高速度&#xff0c;确保…

怎么对视频进行压缩?

怎么对视频进行压缩&#xff1f;视频压缩&#xff0c;我们都知道是将视频文件进行压缩变小的过程&#xff0c;是我们日常办公中较为常用的手段。现如今&#xff0c;在视频技术不断发展与创新的基础上&#xff0c;视频分辨率也在不断提高&#xff0c;进而导致文件占有量也非常大…

前后端分离------后端创建笔记(05)用户列表查询接口(下)

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

设计HTML5图像和多媒体

在网页中的文本信息直观、明了&#xff0c;而多媒体信息更富内涵和视觉冲击力。恰当使用不同类型的多媒体可以展示个性&#xff0c;突出重点&#xff0c;吸引用户。在HTML5之前&#xff0c;需要借助插件为网页添加多媒体&#xff0c;如Adobe Flash Player、苹果的QuickTime等。…

DoIP学习笔记系列:(六)满足AES128-CMAC算法的“安全认证”.dll生成实践

文章目录 1. 算法Demo2. 算法实现传送门 DoIP学习笔记系列:导航篇 AES128-CMAC算法在汽车电子控制单元的软件开发中涉及到安全相关的需求经经常用到,具体的算法原理请各位小伙伴自行百度,本篇主要向大家分享该算法如何集成到.dll文件中,在OTA、刷写等场景作为$27服务的安全…

python -m参数的作用(python3 -m)

文章目录 Python -m 参数的作用直接执行模块代码模块自测试环境隔离避免名称冲突 其他&#xff1a;python3 --help Python -m 参数的作用 在Python中&#xff0c;使用-m参数可以执行一个模块作为脚本。它是用于从命令行直接运行一个Python模块的标志。这种方式具有以下几个方面…

RocketMQ消息轨迹产生的背景以及使用方式

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 背景 最近在维护RocketMQ经常会出现这种问题 消息发送方和接收方出现扯皮&#xff0c;消息发送方说我的消息已经发送成功了&#xff0c;消费方说我没接收到消息。…

uni——初次加载问题处理(赋值后再调用)

案例描述 此案例中 一进页面接收good_id并调用接口&#xff0c;这个流程正常。 这个changeNum也是一进页面就触发了&#xff08;组件购物车加减自带&#xff09;&#xff0c;且触发的顺序在onload赋值id之前&#xff0c;这时候good_id还是为空&#xff0c;所以接口报错。如何处…

十一、避开客户端控件——收集用户数据

文章目录 一、HTML表单1.1 长度限制1.2 基于脚本的确认1.3 禁用的元素 二、浏览器拓展2.1 常见的浏览器拓展技术2.2 攻击浏览器扩展的方法 一、HTML表单 应用程序使用客户端控件限制客户端提交的数据的另一个主要控制对象&#xff0c;是由客户端计算机自己收集的数据。 HTML表单…

Python-OpenCV中的图像处理-图像直方图

Python-OpenCV中的图像处理-图像直方图 图像直方图统计直方图绘制直方图Matplotlib绘制灰度直方图Matplotlib绘制RGB直方图 使用掩膜统计直方图直方图均衡化Numpy图像直方图均衡化OpenCV中的直方图均衡化CLAHE 有限对比适应性直方图均衡化 2D直方图OpenCV中的2D直方图Numpy中2D…

Medical Isolated Power Supply System in Angola

安科瑞 华楠 Abstract: Diagnosis and treatment in modern hospitals are inseparable from advanced medical equipment, which are inseparable from safe and reliable power supply. Many operations often last for several hours, and the consequences of a sudden pow…

欢迎光临,博客网站

欢迎光临&#xff1a;YUNYE博客~https://yunyeblog.com/更多的文章&#xff0c;供大家参考学习&#xff01;&#xff01;&#xff01;

Mybatis 源码 ④ :TypeHandler

文章目录 一、前言二、DefaultParameterHandler1. DefaultParameterHandler#setParameters1.1 UnknownTypeHandler1.2 自定义 TypeHandler 三、DefaultResultSetHandler1. hasNestedResultMaps2. handleRowValuesForNestedResultMap2.1 resolveDiscriminatedResultMap2.2 creat…