【算法】最长公共子序列(C/C++)

news2024/9/20 13:39:48

最长公共子序列(LCS,Longest Common Subsequence)问题简称(LCS),是动态规划里面里面的基础算法它的所解决的问题是,在两个序列中找到一个序列,使得它既是第一个序列的子序列,也是第二个序列的子序列,并且该序列长度最长。由下图中两个序列,我们可以看出来最长公共子序列为[s c r g]。

我们来举个“栗子”,比如序列A为“abcdef”,序列B为“bcef”,那么它的最长公共子序列为序列B,即:“bcef”,注意最长公共子序列不用保证每一个字符必须连续。那么我们一般的暴力做法是什么呢?首先我们先要确定一个参照序列,这里以A为例吧,首先我们需要确定公共子序列的头部,由于选择了A序列为参照序列,那么遍历A序列的每一个字符,把这个遍历的字符与B序列的每一个字符相比较,若相等,A序列遍历到下一个字符,在B序列的基础上再与B序列的下一个字符为起点继续进行比较,直到序列结束,然后再确定A序列的下一个字符为头部,以此类推,从这里面找一个最大的数,即是最长公共子序列的长度。像这样做法,我们的时间复杂度也要O(n^2*m)(n为序列A的长度,m为序列B的长度)。这样的时间复杂度在做题时必然会WA掉,也是面试官不想看到的,我们肯定会有更为优秀的算法,下面我们介绍动态规划的思想。


动态规划:

上面我们说到每次确定公共子序列的头部时,我们的A序列需要重新返回来遍历A序列与B序列寻找相同的字符。这样的操作我们在第一次遍历时就已经遍历过一次,只是没有记录结果,如果我能够把这个结果记录下来,那么下一次再遍历到这个状态我们可以直接拿来用,避免了重复计算,大大减少了计算量,从而减少了时间复杂度。那么我们如何进行记录这个状态呢,我们设一个二维数组dp[i][j],表示A序列的前i项与B序列的前j项所能构成的最长公共子序列长度。

dp[i][j]的状态转移方程分为两种,当A[i]==B[j]时dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);说明当时这两个字符相等,就等于A序列前一个字符跟B序列前一个字符这个状态+1。当A[i]!=B[j]时dp[i][j]=max(dp[i-1][j],dp[i][j-1]);若此时这两个字符不相等,那么就是A序列前一个字符跟B序列当前字符这个状态与B序列前一个字符跟A序列当前字符这个状态进行比较,哪一个大我当前dp[i][j]状态就从哪里转移。

 for(int i=1;i<=n;i++){
    for(int j=1;j<=m;j++)
     {
     	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
     	if(A[i]==B[j])
     	dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
     }
 }

此时时间复杂度来到了O(n*m)(n为序列A的长度,m为序列B的长度),这样便可以解决大部分题目,有的题目还是解决不了的,对于更高级一点我们可以利用二分优化一下。时间复杂度便可以达到了O(nlog(n)),具体怎么实现下面我们讲解一下。


二分优化:

二分优化就是利用离散化操作,把两个数组通过映射为一个数组,在一个数组里面类似于求最长上升子序列操作,我们选择一个参照数组a,那么就要遍历数组b,考虑它的映射值大小与dp数组值得关系,其核心就一句口诀“大则添加,小则替换”。

解释一下什么意思。考虑新进来一个元素a[i]:

(1)大则添加:如果a[i]大于b[len],直接让b[++len]=a[i]。即b数组的长度增加1,而且添加了一个元素。

(2)小则替换:如果a[i]小于或等于b[len],就用a[i]替换掉b数组中第一个大于或等于a[i]的元素。

假设第一个大于a[i]的元素是b[j],那么用a[i]换掉b[j]后,会使得b[1...j]这个上升子序列的结尾元素更小。对于一个上升子序列,其结尾元素越小,越有利于续接其它元素,也就越可能变得更长,也就是说替换完使序列更有潜力,更容易接纳元素。

int a[105]={1,6,3,2,7,4,3,3,2};
int b[105];
int m=9;
int len=1;
b[1]=a[1];
int find(int x){//二分查找
	int L=1,R=len,mid;
	while(L<=R){
		mid=(l+r)>>1;
		if(x>b[mid])L=mid+1;
		else R=mid-1;
	}
	return L;
}

for(int i=2;i<=n;i++){
	if(a[i]>b[len]){//大则添加
		b[++len]=a[i];
	}else{//小则替换
		j=find(a[i]);
		b[j]=a[i];
	}
}
printf("%d\n",len);
图解算法:

文字去描述二分优化的过程不太好描述跟理解,那么我们进行图解一下算法的实现过程,希望对大家有所帮助。

我们以数组A=[3,1,4,2],数组B为[2,1,3,4]为例,进行图解。

初始化:离散化操作,对数组A进行离散化处理,得到map映射数组,拿着这个映射数组去把B数组的映射数组求出来。

第一步:预处理部分做完了就要开始我们的真正的实现了。当前我们初始化了dp数组为无穷大,由于我们选取了数组A为参照数组,那么我们就去遍历数组B的映射数组,这里就用到了我们所说的口诀“大则添加,小则替换”,此时数组B的映射数组第一个为4,dp数组里面都是inf,4<inf,小则替换,我们就去dp数组里面寻找第一个大于等于4的位置,给它替换成4,很明显dp数组第一个位置(下标为0)由inf替换成4。

第二步:数组B的映射数组到了第二个数了(下标为1),dp里面此时有一个数了,当前遍历的数为2,2与当前dp位置上的数比较,2<4,小则替换,很明显把dp第一个位置上的数4替换成2。

第三步:此时遍历到第三个数(下标为2),当前数组B的映射数组的值为1,1与当前dp数组上的位置相比较,1<2,小则替换,则把2替换为1。

第四步:此时遍历到最后一个位置了,当前数组B的映射数组的值为3,3与dp数组上当前位置上的数进行比较,3>1,根据口诀大则添加,则把3加到当前dp位置后面,即把dp[1]=3。

最终dp的长度为2,那么最长公共子序列的长度的值为2。由此dp数组我们还可以得到最长公共子序列是哪一个序列,这样我们反推回去,当前dp[0]=1,dp[1]=3,1对应的映射为3,3对应的映射为4,那么我们所得到的最长公共子序列就是[3,4]。


原题链接:【模板】最长公共子序列 - 洛谷

题目描述

给出 1,2,…,n 的两个排列P1​ 和 P2​ ,求它们的最长公共子序列。

输入格式

第一行是一个数 n。

接下来两行,每行为 n 个数,为自然数 1,2,…,n 的一个排列。

输出格式

一个数,即最长公共子序列的长度。

输入 

5 
3 2 1 4 5
1 2 3 4 5

输出 

3

说明/提示

对于 50%的数据, n≤10^3;

对于 100%的数据,n≤10^5。


解题思路:

最长公共子序列有两种解法,分别是朴素解法和一种二分优化的解法,此题10^5,若用第一种朴素解法肯定会TLE,所以下面我们详细介绍第二种解法。

朴素解法(会TLE)

很明显我们去枚举序列1的每一位和序列2的每一位,如果两个数字相等,那么dp[i][j]=dp[i-1[j-1]+1。最后计算dp[n][n]即可。

代码实现:
#include<iostream>
using namespace std;
const int N=1005;
int dp[N][N],a1[N],a2[N],n;
int main()
{
   //dp[i][j]表示两个串从头开始,直到第一个串的第i位 
   //和第二个串的第j位最多有多少个公共子元素 
   cin>>n;
   for(int i=1;i<=n;i++)cin>>a1[i];
   for(int i=1;i<=n;i++)cin>>a2[i];
   for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
     {
     	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
     	if(a1[i]==a2[j])
     	dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
     	//因为更新,所以++; 
     }
   cout<<dp[n][n]<<endl;;
   return 0;
}

优化解法

主要跟最长上升子序列的优化方法一样的,记住这句话就可以,“大则添加,小则替换”,这就是实现的思路,当此时要进入的值大于最长子序列的最后值就添加,若小于最长子序列的最后的值,则找到最长子序列中第一个大于此值的下标把它给替换掉。

代码实现:
#include<iostream>
using namespace std;
const int N=1e5+5;
int n,len=1;
int a[N],b[N],dp[N],map[N];//mapA映射B,相当于A数组当标准,操作B数组,压缩为一个数组,
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],map[a[i]]=i;//map映射
	for(int i=1;i<=n;i++)cin>>b[i],dp[i]=0x3f3f3f;//初始无穷大
	for(int i=1;i<=n;i++){
		if(map[b[i]]>dp[len])dp[++len]=map[b[i]];//大则添加
		else dp[lower_bound(dp,dp+len,map[b[i]])-dp]=map[b[i]];//小的替换,lower_bound实现更简单
	}
	cout<<len<<endl;//最后输出长度即可
	return 0;
}

最长公共子序列(LCS)是算法动态规划之中最基础的部分,是每一位算法初学者的首选,也是数学之中必学的内容,文章尚有不足,若有错误的地方恳请各位大佬指出。

执笔至此,感触彼多,全文将至,落笔为终,感谢大家的支持。 

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

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

相关文章

SpringCloud微服务实现服务降级的最佳实践

Spring Cloud是一种用于快速构建分布式系统的框架&#xff0c;它提供了许多有用的功能&#xff0c;其中包括服务降级。 服务降级是一种保护机制&#xff0c;它可以在面临高并发或故障时保持服务的稳定性。当系统资源不足或服务出现故障时&#xff0c;服务降级可以通过关闭一些功…

2.计算机网络基础

2. 计算机网络基础 (1) 计算机网络的定义 计算机网络是指将地理位置不同、具有独立功能的多个计算机系统通过通信线路和设备连接起来,以功能完善的网络软件实现网络中资源共享的系统。最简单的定义是:计算机网络是一些互相连接的、自治的计算机系统的集合。最庞大的计算机网…

MATLAB系列03:分支语句和编程设计

MATLAB系列03&#xff1a;分支语句和编程设计 3. 分支语句和编程设计3.1 自上而下的编程方法简介3.2 伪代码的应用3.3 关系运算符和逻辑运算符3.3.1 关系运算符3.3.2 小心和~运算符3.3.3 逻辑运算符3.3.4 逻辑函数 3.4 选择结构3.4.1 if结构3.4.2 switch结构3.4.3 try/catch结构…

c++的decltype关键字

它可以将变量声明为表达式指定的类型

C语言程序二级 之知识点 程序填空 程序设计 程序修改

一 知识点 宏定义是指用一个宏名(名字)来代表一个字符串。宏定义的功能是在编译预处理时&#xff0c;对程序中所有出现的"宏名"都用宏定义中的字符串去代换&#xff0c;这称为"宏代换"或"宏展开"。无参宏定义的一般格式&#xff1a;#define 标识…

中国光刻机突破28nm?进步巨大但前路漫漫

在近期的报道中&#xff0c;中国国产光刻机进入推广目录的消息引发了广泛关注&#xff0c;其中提到的一款氟化亚光刻机的分辨率达到了65nm&#xff0c;被视作具备28nm制程节点的生产能力。那么&#xff0c;国产光刻机真的已经突破了28nm制程节点了吗&#xff1f;本文将对相关技…

Mysql梳理6——order by排序

目录 6 order by排序 6.1 排序数据 6.2 单列排序 6.3 多行排列 6 order by排序 6.1 排序数据 使用ORDER BY字句排序 ASC&#xff08;ascend&#xff09;:升序DESC(descend):降序 ORDER BY子句在SELECT语句的结尾 6.2 单列排序 如果没有使用排序操作&#xff0c;默认…

第157天: 安全开发-Python 自动化挖掘项目SRC 目标FOFA 资产Web 爬虫解析库

案例一&#xff1a;Python-WEB 爬虫库&数据解析库 这里开发的内容不做过多描述&#xff0c;贴上自己写的代码 爬取数据 要爬取p标签&#xff0c;利用Beautyfulsoup模块 import requests,time from bs4 import BeautifulSoup#url"https://src.sjtu.edu.cn/rank/firm…

教你建设智慧数字乡村如何选供应商,如何落地项目

说到数字乡村建设&#xff0c;大家都有自己的思路和想法&#xff0c;那么如果现在让你来做这个项目你又如何来实施&#xff1f;都需要什么方式&#xff1f;都要具备什么条件&#xff1f; 下面我来说一下我的个人思路&#xff0c;要做数字乡村我觉得前提是必须满足几个条件&…

可视化工具Gephi安装要求和特点

Gephi是进行社会图谱数据可视化分析的工具&#xff0c;不但能处理大规模数据集并且Gephi是一个可视化的网络探索平台&#xff0c;用于构建动态的、分层的数据图表。 Gephi安装要求 ① gephi是一个可多平台使用的绘图软件&#xff0c;能在Windows&#xff0c;OS&#xff0c;Lin…

平价头戴式蓝牙耳机有哪些?四款公认平价性能超强品牌机型推荐

在追求高品质音乐体验的同时&#xff0c;许多消费者希望找到价格亲民的头戴式蓝牙耳机&#xff0c;市场上不乏性能卓越、价格实惠的产品&#xff0c;它们凭借出色的音质、舒适的佩戴体验和可靠的续航能力赢得了用户的青睐&#xff0c;那么在众多的头戴式蓝牙耳机内&#xff0c;…

英伟达:AI时代的领跑者,引领智能计算的未来@附149页PDF文件下载

在人工智能的浪潮中&#xff0c;英伟达&#xff08;NVIDIA&#xff09;以其卓越的GPU技术&#xff0c;成为了这个时代的领跑者。从游戏显卡的霸主到AI计算的领导者&#xff0c;英伟达的转型之路充满了创新与突破。今天&#xff0c;我们将深入探讨2024年英伟达如何通过其战略布局…

Python 中的异步编程:从入门到实践

在现代编程实践中&#xff0c;异步编程已经成为一个不可或缺的技能&#xff0c;尤其是在处理高并发和I/O密集型应用时。Python&#xff0c;作为一种动态、解释型的高级编程语言&#xff0c;提供了强大的异步编程支持&#xff0c;使得开发者能够有效地编写高效、可扩展的应用程序…

虹科技术 | Linux环境再升级:PLIN驱动程序正式发布

Linux驱动程序领域再添新成员&#xff0c;PLIN驱动程序现已正式发布。这一新驱动程序为使用LIN接口的用户提供了一个便捷、高效的解决方案。本文将展示如何安装PLIN驱动程序&#xff0c;以及如何在Linux环境下进行基本的PLIN通信操作&#xff0c;确保您能够快速掌握并应用这一新…

寄存器二分频电路

verilog代码 module div2_clk ( input clk, input rst,output clk_div);reg clk_div_r; assign clk_div clk_div_r;always(posedge clk) beginif(rst)beginclk_div_r < 1b0;endelsebeginclk_di…

射击靶标检测系统源码分享

射击靶标检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

pytorch-AutoEncoders实战之VAE

目录 1. VAE回顾2. KL的计算公式3. 构建网络4. 模型训练 1. VAE回顾 VAE Variational Auto Encoder&#xff0c;变分自编码器。是一种常见的生成模型&#xff0c;属于无监督学习的范畴。它能够学习一个函数/模型&#xff0c;使得输出数据的分布尽可能的逼近原始数据分布&…

CCRC-CDO首席数据官:未成年人首次上网年龄持续降低

近日&#xff0c;中国社会科学院新闻与传播研究所联合社会科学文献出版社发布了《青少年蓝皮书&#xff1a;中国未成年人互联网运用报告(2024)》&#xff0c;该报告对中国未成年人的互联网使用情况进行了全面的研究和专项汇报。 调查数据透露&#xff0c;未成年人接触网络的年…

光耦选型 | 充电领域使用光耦型号推荐——晶体管光耦KL3H7

在充电领域&#xff0c;光耦作为一种常见的光电耦合器件&#xff0c;通常用于电气隔离、信号传输、电池保护和充电控制等方面。 电源气隔离&#xff1a;光耦可用于实现电源气隔离&#xff0c;将输入和输出电路进行隔离&#xff0c;提高系统的安全性和稳定性。 信号传输&#…

0基础也可以转行做产品经理吗?

转行成为产品经理&#xff0c;即使没有相关工作经验或技术背景&#xff0c;仍然是一个可行的目标。产品经理的职责多样&#xff0c;但成功的产品经理通常需要具备一系列的技能和素养&#xff0c;包括项目管理、市场分析、用户体验设计等。在没有相关经验的情况下&#xff0c;通…