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

这篇具有很好参考价值的文章主要介绍了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);  

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

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

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

​ 也就是说,当原始问题中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: 在海滩上捡贝壳时,当前拾到了一个,回家时最终能拾到的贝壳一定是当前拾到的这一个加上后续所拾到的贝壳。

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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()时,说明已经扫描到了子符串的最后。如下图所示,无论哪一个指针先到达字符串的末尾,则都不再存在任何公共子序列。

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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 重叠子问题

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

并且可以看到 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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

  • 如图,让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时最长公共子序列的值。

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

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

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

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

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

编码实现:

#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;
}

测试结果:

C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性,C++编程之美,c++,动态规划,代理模式

4. 总结

最长公共子序列很有代表性,分析基于递归和动态规划的实现过程,可以帮助我们理解此类问题,且解决此类问题。文章来源地址https://www.toymoban.com/news/detail-648620.html

到了这里,关于C++ 动态规划经典案例解析之最长公共子序列(LCS)_窥探递归和动态规划的一致性的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 动态规划--最长公共子序列

    动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题﹐ 即将大规模变成小规模 ,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是﹐适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。 他们之间有关系

    2024年02月04日
    浏览(73)
  • 动态规划——最长公共子序列

    先来讲解以下什么是最长公共子序列。最长公共子序列不是最长相同字符串,有点相似但不一样,来举个简单的例子,有字符串s1=bcdea,s2=abce,最长相同字符串是bc,最大公共部分是2;而最长公共子序列则是bce,最大公共部分是3。可以看出,公共子序列不需要连续相等,有相

    2023年04月19日
    浏览(49)
  • 最长公共子串(动态规划)

    查找两个字符串a,b中的最长公共子串_牛客题霸_牛客网 1.找a 和 b 的最长公共子串实际上是在a的子串和b的子串中找最长公共子串 ins[i][j]实际上记录的就是 以a的第i个字符和以b的第j个字符结尾的子串中存在的最长公共子串的长度 接下来我们举个例子: a: abcabcde b: abcd ins[1][1]

    2023年04月12日
    浏览(47)
  • 【算法-动态规划】最长公共子序列

    💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老 导航 檀越剑指大厂系列:全面总

    2024年01月23日
    浏览(46)
  • 算法:动态规划——最长公共子序列

    动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。 与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法解这类问题,则分解得到的

    2023年04月27日
    浏览(58)
  • 【动态规划】最长公共子序列(Java)

    给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

    2024年01月18日
    浏览(74)
  • 动态规划之最长公共子序列模板

    夏令营:动态规划特训 - 【算法模板题】最长公共子序列 - 蓝桥云课 (lanqiao.cn) 我们来解释一下状态转移方程吧。 dp[i][j]的含义是第i个和第j个字符,i与j的下标从1开始,代表着原子符串的第一个字符。那么理所当然字符串a的第0个字符和字符串b的0个字符的公共长度为0.如果字

    2024年02月12日
    浏览(45)
  • 动态规划-最长公共子序列(c语言)

    实验 3: 最长公共子序列 内容: 给定两个字符串str1和str2,输出两个字符串的最长公共子序列,如果最长公共子序列为空,则返回“-1”。目前给出的数据,仅仅会存在一个最长的公共子序列。 数据范围: 0 ≤|str1|,|str2|≤2000 要求: 空间复杂度O(n 2 ) 具体思路: step1:

    2024年02月04日
    浏览(50)
  • 【动态规划】最长公共子序列Python实现

    个人主页:丷从心 系列专栏:动态规划算法 问题描述 给定两个序列 X = {   x 1 , x 2 , ⋯   , x m   } X = set{x_{1} , x_{2} , cdots , x_{m}} X = { x 1 ​ , x 2 ​ , ⋯ , x m ​ } 和 Y = {   y 1 , y 2 , ⋯   , y n   } Y = set{y_{1} , y_{2} , cdots , y_{n}} Y = { y 1 ​ , y 2 ​ , ⋯ , y n ​ } ,找出 X X X

    2024年02月20日
    浏览(47)
  • 动态规划-----最长公共子序列(及其衍生问题)

    目录 一.最长公共子序列的基本概念: 解决动态规划问题的一般思路(三大步骤): 二.最长公共子序列题目: 三.字符串的删除操作: 四.最小 ASCII 删除和: 首先需要科普一下,最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么

    2024年03月26日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包