面试算法题精讲:最长公共子序列

news2024/11/20 1:28:53

面试算法题精讲:最长公共子序列

题面

题目来源:1143. 最长公共子序列

题目描述:

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列(LCS)的长度。如果不存在公共子序列 ,返回 0 。

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。

两个字符串的公共子序列是这两个字符串所共同拥有的子序列。

解法1:记忆化搜索

代码:

// 记忆化搜索

class Solution
{
public:
    int longestCommonSubsequence(string s, string t)
    {
        int n = s.length(), m = t.length();
        int memo[n][m];
        memset(memo, -1, sizeof(memo)); // -1 表示没有访问过
        function<int(int, int)> dfs = [&](int i, int j) -> int
        {
            if (i < 0 || j < 0)
                return 0;
            int &res = memo[i][j];
            if (res != -1)
                return res;
            if (s[i] == t[j])
                return res = dfs(i - 1, j - 1) + 1;
            return res = max(dfs(i - 1, j), dfs(i, j - 1));
        };
        return dfs(n - 1, m - 1);
    }
};

结果:

在这里插入图片描述

复杂度分析:

时间复杂度:O(nm),其中 m 和 n 分别是字符串 s 和 t 的长度。

空间复杂度:O(nm),其中 m 和 n 分别是字符串 s 和 t 的长度。

解法2:动态规划

代码:

// 动态规划

class Solution
{
public:
    int longestCommonSubsequence(string text1, string text2)
    {
        // 特判
        if (text1.empty() || text2.empty())
            return 0;
        int len1 = text1.size(), len2 = text2.size();
        // 状态数组,并初始化
        //  dp[i][j]表示到text1的位置i为止、到text2的位置j为止、最长的公共子序列长度
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        // 状态转移
        for (int i = 1; i <= len1; i++)
            for (int j = 1; j <= len2; j++)
            {
                if (text1[i - 1] == text2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        return dp[len1][len2];
    }
};

结果:

在这里插入图片描述

复杂度分析:

时间复杂度:O(nm),其中 m 和 n 分别是字符串 s 和 t 的长度。

空间复杂度:O(nm),其中 m 和 n 分别是字符串 s 和 t 的长度。

解法3:动态规划:空间优化

代码:

// 动态规划:空间优化

class Solution
{
public:
    int longestCommonSubsequence(string s, string t)
    {
        int n = s.length(), m = t.length(), dp[2][m + 1];
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
            {
                if (s[i] == t[j])
                    dp[(i + 1) % 2][j + 1] = dp[i % 2][j] + 1;
                else
                    dp[(i + 1) % 2][j + 1] = max(dp[i % 2][j + 1], dp[(i + 1) % 2][j]);
            }
        return dp[n % 2][m];
    }
};

结果:

在这里插入图片描述

复杂度分析:

时间复杂度:O(nm),其中 m 和 n 分别是字符串 s 和 t 的长度。

空间复杂度:O(nm),其中 m 是字符串 t 的长度。

进阶:求最长公共子序列的内容

前面我们只是通过动态规划求得了两个字符串的最长公共子序列长度。我们要怎么样根据dp得到我们想要的LCS序列字符数组呢?

原理:由于我们填表的时候,当找到一个公共字符我们就会将dp[i][j]的值设置为dp[i-1][j-1] + 1的值,那么这个值也就是我们根据dp表找公共子序列的那个字符。

图解:

在这里插入图片描述

寻找字符的过程:

  1. 从 dp[m][n] 开始回溯;
  2. 如果 dp[i][j] == dp[i-1][j-1] + 1(即dp[i][j] != dp[i-1][j] && dp[i][j] != dp[i][j-1]),说明 s[i-1] == t[j-1],我们找到了一个公共字符,插入lcs中,i–,j–,往左上角回溯;
  3. 否则,如果 dp[i][j] == dp[i-1][j],i–,往上回溯;如果 dp[i][j] == dp[i][j-1],j–,往左回溯。
  4. 由于lcs是逆序插入公共字符的,我们把lcs倒序一下,lcs就是最长公共子序列字符串了。

在这里插入图片描述

在这里插入图片描述

最终情况:

在这里插入图片描述

《算法导论》中回溯输出最长公共子序列过程:

在这里插入图片描述

实现代码:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
	string s = "ABCABCDEFG";
	string t = "ABXYZCCDEXYZ";

	int n = s.size(), m = t.size();
	// dp[i][j]表示到s的位置i为止、到t的位置j为止、最长的公共子序列长度
	vector<vector<int> > dp(n + 1, vector<int>(m + 1, 0));
	// 状态转移
	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]);
		}

	int lscLen = dp[n][m];
	cout << lscLen << endl;

	string lsc = string(lscLen, 0);
	int i = n, j = m, k = lscLen, idx = 0;
	while (k)
	{
		if (dp[i][j] == dp[i - 1][j])
			i--;
		else if (dp[i][j] == dp[i][j - 1])
			j--;
		else
		{
			lsc[idx++] = s[i - 1];
			i--;
			j--;
			k--;
		}
	}
	reverse(lsc.begin(), lsc.end());
	cout << lsc << endl;

	system("pause");
	return 0;
}

我们还可以把整个过程包装成一个函数:

string longestCommonSubsequence(string s, string t)
{
	// 特判
	if (s.empty() || t.empty())
		return 0;
	int n = s.size(), m = t.size();
	//  dp[i][j]表示到s的位置i为止、到t的位置j为止、最长的公共子序列长度
	vector<vector<int> > dp(n + 1, vector<int>(m + 1));
	// state
	vector<vector<int> > state(n + 1, vector<int>(m + 1));
	// 初始化
	for (int i = 0; i <= n; i++)
		dp[i][0] = 0;
	for (int j = 0; j <= m; j++)
		dp[0][j] = 0;
	// 状态转移
	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;
				state[i][j] = 0; // 为了后面回溯,作为公共子序列的判定
			}
			else if (dp[i - 1][j] >= dp[i][j - 1])
			{
				dp[i][j] = dp[i - 1][j];
				state[i][j] = 1; // 为了后面向上回溯,向上寻找子序列的判定
			}
			else
			{
				dp[i][j] = dp[i][j - 1];
				state[i][j] = 2; // 为了后面向左回溯,向左寻找子序列的判定
			}
		}
	int lscLen = dp[n][m];
	string lsc = string(lscLen, 0);
	int i = n, j = m, k = lscLen - 1;
	while (k >= 0)
	{
		if (state[i][j] == 0) // 找到了公共字符,往左上回溯 
		{
			lsc[k--] = s[i - 1];
			i--;
			j--;
		}
		else if (state[i][j] == 1) // 向上回溯 
			i--;
		else // 向左回溯 
			j--;
	}
	return lsc;
}

在动态规划求LCS的长度时,用state数组标记转移状态,在求LCS时参考state就能知道往哪个方向回溯了。

参考

  1. https://blog.csdn.net/weixin_54408360/article/details/126925084
  2. https://blog.csdn.net/lxt_Lucia/article/details/81209962
  3. https://blog.csdn.net/2301_77329667/article/details/136479874

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

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

相关文章

浅谈大数据时代下的电商风控||电商数据API接口

抢抢抢&#xff01;最后1天&#xff0c;双十一直播活动来啦&#xff01;抢直播专属优惠…… 视频号 随着大数据时代的兴起&#xff0c;互联网电商风控已经从无风控、人工抽取规则为主的简易规则模型发展到当前基于大数据的风控。与金融风控不同&#xff0c;互联网电商风控呈现出…

vue3 ——笔记 (条件渲染,列表渲染,事件处理)

条件渲染 v-if v-if 指令用于条件性地渲染一块内容&#xff0c;只有v-if的表达式返回值为真才会渲染 v-else v-else 为 v-if 添加一个 else 区块 v-else 必须在v-if或v-else-if后 v-else-if v-else-if 是v-if 的区块 可以连续多次重复使用 v-show 按条件显示元素 v-sh…

[Java]线程生命周期与线程通信

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/138101131 出自【进步*于辰的博客】 线程生命周期与进程有诸多相似&#xff0c;所以我们很容易将…

【Vue】可拖拽排序表格组件的实现与数据保存

1、描述 使用el-table-draggable组件来创建一个可拖拽的表格。 表格中的数据存储在tableData数组中&#xff0c;每个对象都有sortOrder、id、name和age属性。 当用户拖拽表格行并释放时&#xff0c;handleRowOnEnd方法会被调用&#xff0c; 更新tableData中每个对象的sortO…

super与this

目录 原型链与继承继承中的原型链 classsuper与this 我们可能会对一个问题感到好奇&#xff1a;为什么在派生类中&#xff0c;我们需要在调用this之前调用super。我们通常将其视为一种规范&#xff0c;却很少深入探究这个规范的真正意义。许多人认为super不过是ES6之前继承方式…

vercel.app 部署的服务国内无法最优解决方案

今天在部署 waline 评论系统时&#xff0c;发现国内 IP 无法访问&#xff0c;这将导致评论系统无法使用&#xff0c;由于我的站点是技术站点需要和用户评论沟通&#xff0c;但是如果评论无法使用的话就会导致流失更多的用户&#xff0c;通过自己实际测试后发现一个最优的解决方…

【IR 论文】DPR — 最早提出使用嵌入向量来检索文档的模型

论文&#xff1a;Dense Passage Retrieval for Open-Domain Question Answering ⭐⭐⭐⭐⭐ EMNLP 2020, Facebook Research Code: github.com/facebookresearch/DPR 文章目录 一、论文速读二、DPR 的训练2.1 正样本和负样本的选取2.2 In-batch negatives 技巧 三、实验3.1 数据…

编写一个函数fun,它的功能是:实现两个字符串的连接(不使用库函数strcat),即把p2所指的字符串连接到p1所指的字符串后。

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 编写…

长度最小的子数组 ---- 滑动窗口

题目链接 题目: 分析: 解法一:暴力解法, 找到所有连续子数组, 保留满足条件的解法二: 利用滑动窗口 找子数组 因为数组中都是正整数, 通过进窗口的操作, 我们找到一组, 如示例一中的2,3,1,2, 判断满足和>7, 那么根据单调性, 我们就不再需要判断加上后面两个数的两个子数组…

在 GitHub 中掌握键盘快捷键的简短指南

你是否知道 GitHub 几乎每页都有键盘快捷键&#xff1f;这篇文章将带你探索 GitHub 的键盘快捷键世界&#xff0c;以及它们如何帮助你快速导航和执行操作。 读完这篇&#xff0c;你将能够&#xff1a; 掌握快捷键&#xff1a;想知道如何访问这些快捷键&#xff1f;在任何 Git…

记录浏览器打开网站拦截提示不安全解决方法

浏览器可能会因为多种原因显示“不安全”的警告,这通常是由于安全设置不当或配置错误造成的。以下是一些常见的原因和解决方法: 1. HTTPS未启用 原因:如果网站使用HTTP而不是HTTPS,浏览器可能会显示不安全的警告。 解决方法:配置SSL/TLS证书并使用HTTPS来加密数据传输…

64、二分-搜索二维矩阵

思路&#xff1a; 通过使用二分方式&#xff0c;对于每行进行二分&#xff0c;因为每行的最后一个数小于下一行的第一个数&#xff0c;我们就可以依次二分。首先取出行数N&#xff0c;然后从0-N进行二分&#xff0c;如果mid最后一个数小于目标值说明0-mid中没有&#xff0c;舍弃…

开源博客项目Blog .NET Core源码学习(19:App.Hosting项目结构分析-7)

本文学习并分析App.Hosting项目中后台管理页面的主页面。如下图所示&#xff0c;开源博客项目的后台主页面采用layui预设类layui-icon-shrink-right设置样式&#xff0c;点击主页面中的菜单&#xff0c;其它页面采用弹框或者子页面形式显示在主页面的内容区域。   后台主页面…

Qt设置可执行程序图标,并打包发布

一、设置图标 图标png转ico: https://www.toolhelper.cn/Image/ImageToIco设置可执行程序图标 修改可执行程序图标 添加一个rc文件,操作如下,记得后缀改为rc 打开logo.rc文件添加代码IDI_ICON1 ICON DISCARDABLE "logo.ico"在项目pro后缀名的文件中添加代码 RC_…

OpenWrt上的docker容器无法访问外网解决

容器里能ping通OpenWrt的管理地址和wan口地址&#xff0c;但ping外网别的ip或域名就无法访问 简单修改设置就可以&#xff1a; Luci>网络>防火墙>转发&#xff1a;接受 ->保存应用

Linux中的yum和gcc/g++

一、快速认识yum&#xff08;简单介绍&#xff09; 在Linux中&#xff0c;我们也要进行工具/指令/程序、安装、检查、卸载等等&#xff0c;需要使用到yum 在Linux中安装软件的方式&#xff1a; 源代码安装——交叉编译的工作rpm包直接安装yum/apt-get yum:yum是我们Linux预…

在no branch上commmit后,再切换到其他分支,找不到no branch分支的修改怎么办?

解决办法 通过git reflog我们可以查看历史提交记录&#xff0c;这里的第二条提交&#xff08;fbd3ea8&#xff09;就是我在no branch上的提交。 再通过git checkout -b backup fbd3ea8&#xff0c;恢复到上次提交的状态&#xff0c;并且为其创建个分支backup&#xff0c;此时…

FTP可替代?传输替代方案应该需要具备哪些条件?

企业对数据传输的安全性、速度和稳定性的要求日益提高。传统的FTP虽然在早期广泛使用&#xff0c;但随着技术的发展和业务需求的增长&#xff0c;其局限性逐渐显现&#xff0c;迫切需要替代方案以满足现代企业的需求。 FTP的局限性主要表现在以下几个方面&#xff1a; 安全性不…

Postman,一个功能强大的API开发和测试工具

最近有小伙伴说在找 postman 的使用教程&#xff0c;案例等文章。 那么今天我就来写一个。 Postman 是一个功能强大的 API 开发和测试工具&#xff0c;它提供了丰富的功能&#xff0c;帮助开发人员更好地管理、测试和文档化 API。无论是单独开发还是团队协作&#xff0c;Postma…

一个数据人眼中的《上游思维》

最近读了《上游思维》这本书&#xff0c;很受启发&#xff0c;我想从一个数据人的角度来聊一聊我对这本书的读后感。上游思维本质上是帮助我们解决问题&#xff0c;我发现在解决问题相关的每个阶段&#xff1a;发现问题、找到解决问题的方法、解决问题的过程中、评估问题以及预…