百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

最长公共子串求解套路

toyiye 2024-05-25 20:11 21 浏览 0 评论

作者 | 码海

出品 | 码海(ID:seaofcode)

头图 | CSDN 下载自东方IC

前言

动态规划是大厂的热门考点,其中最长公共子串与最长公共子序列这两道题出现得尤其频繁,这两道题其实有挺多变种,很适合考察侯选人对动态规划的掌握情况,今天我们就先来看看如何求解最长公共子串,图文并茂,清晰易懂!

最长公共子串

题目如下:

输入: x = ["", "a", "b", "c", "a", "d", "f"]
y = ["", "a", "c", "b", "a", "d"],

输出: 2
解释: 最长公共子串为 ad,所以结果为 2

这里需要简单解释下子串与子序列的区别,子串要求这串字符串在原字符串中是连续的,而子序列可以不连续,两者的区别如下:

如果不清楚啥是动态规划的同学,强烈建议先学习下此文,其对动态规划的分析,解题套路中有详细的阐述,好评如潮!对理解动态规划有很大的帮助。

首先看下,如果这题如果用暴力求解的话,应该算出每个字符串的所有子字符串,然后再对所有子字符串进行比较,是个排列组合问题,时间复杂度很大,不可取!所以我们看下怎么用动态规划来解。

解动态规划需要至少明确以下三个概念

  • base case

  • 状态转移方程

  • 自下而上

动态规划一言以蔽之就是利用 「base case 」和「状态转移方程」然后「自下而上」得求得最终问题的解。相对递归的自上而下求解造成的大量子问题的计算,自下而上意味着每个子问题不会被重复计算,很好地达到了「剪枝」的效果。这其中「状态转移方程」的求解无疑是重要的,如果求得了「状态转移」方程,问题其实就解决地差不多了。

回到最长公共子串本身来看,我们来看看它的「状态转移方程」和「base case」是啥。

1、状态转移方程

这题的状态转移方程该怎么定义呢,首先我们求的是两个字符串的公共子串,所以应该意识到这个 dp 方程是个二维数组 dp[i][j],代表的 x 的前 i 个字符串与 y 的 前 j 个字符串的最长公共子串,那么状态状态方程该怎么得出来呢,一般对于字符串类的动态规划来说,我们可以利用「状态转移表」法来帮助我们得出状态转移方程

观察出规律了吗,对于 dp[i][j] 来说,它的值有两种可能,取决于字符 x[i] 和 y[j] 这两个字符是否相等

如果两个字符相等,则 dp[i][j] = dp[i-1][j-1] + 1(注意图中的箭头), 怎么理解,1 代表 x 的第 i 个字符与 y 的第 j 个字符相等,根据 dp[i][j] 的定义,那么它自然等于 x 的前 i-1 个字符串与 y 的 前 j-1 个字符串的最长公共子串 + 1,如下图示

如果 x 的第 i 个字符与 y 的第 j 个字符不相等,那么显然 dp[i][j] = 0,因为 dp[i][j] 定义为最长公共子串,所以只要有一个字符不相等,就说明不满足最长公共子串这个定义,自然 dp[i][j] 为 0 了。

根据以上条件可知状态转移方程为

2、base case

显然图中黄色格子所示为 base case

即可用如下公式表示

这也比较好理解,dp[0][i] 或 dp[j][0] 即空字符串与任意字符串的最大公共子串显然为0。

3、求解

有了状态转移方程,有了 base case, 求解不是难事,需要注意的是最长公共子串长度并不是右下角方格子的数字(dp[5][6]),而是 dp[5][5],这与一般的动态规划解题套路有些不同,我们可以用一个临时变量来表示最长公共子串。

代码如下:

public class Solution { public static int getLCS(char[] x, char[] y) { // base case,默认 dp 中的每个元素都为 0。 int dp = new int[x.length][y.length]; // 最长公共子串, 用一个临时变量表示 int resultLCS = 0; for (int i = 1; i < x.length; i++) { for (int j = 1; j < y.length; j++) { // 以下是状态转移方程 if (x[i] == y[j]) { dp[i][j] = dp[i-1][j-1] + 1; resultLCS = Math.max(resultLCS, dp[i][j]); } else { dp[i][j] = 0; } } } return resultLCS; }
public static void main(String[] args) { char x = {' ', 'a', 'c', 'b', 'a', 'd'}; char y = {' ', 'a', 'b', 'c', 'a', 'd', 'f'}; int lcs = Solution.getLCS(x, y); System.out.println("lcs = " + lcs); }}

求解完成,但别忘了分析时间和空间复杂度,看下是否有优化的空间,这两点是很多同学做算法题常见遗漏的地方。

先来看下时间复杂度,两重循环,所以时间复杂度显然为 O(n^2)。空间复杂度呢,二维数组,所以是 O(n^2),时间复杂度没法优化了,那么空间复杂度呢,其实可以优化为 O(n), 怎么优化,这里就要提一下动态规划的「无后效性」了。

对于各个格子中的数字,它只与其上一行的左上角格子数字有关,与上一行之前的行无关(所以在计算 i = 4 行的格子中,图中 0,1,2 行的格子无需保留),这叫无后效性。

上图中 i=4, j=4 对应的格子中的数字 1 只与其左上角的格子数字 0 有关,所以我们要有一个数组记录其上一行的数字,另外 1 所在行的格子中的数字也要记下来,因为它下一行中格子的数字也是基于本行(1 所在行)的数字推导而来,比如 2 就是基于 1 推导而来的。

由此分析我们要有两个一维数组保留两行的数据,这里的两个数组其实是滚动数组,啥意思呢,我简单解释一下

比如当要计算箭头所指行中格子的数字时,显然,它只与 dp[1] 中格子的数字有关,而与 dp[0] 所指行无关,所以当前行格子的计算结果可以根据 dp[1] 计算出结果放在 dp[0] 中,计算后如下:

现在要计算当前行的格子数,它只与 dp[0] 有关,而与 dp[1] 无关,所以当前行的格子数字可以根据 dp[0] 计算出结果保存在 dp[1] 中。到底是取 dp[0] 还是 dp[1] 呢,这里有一个巧妙的方法:将「当前行&1」的结果与 1 进行比较,比如相等,也就是当前行数为奇数时,则它的结果依赖于 dp[0],当前行的结果写入 dp[1] 中,如果不相等,也就是当前行为偶数时,则它的结果依赖于 dp[1],当前行的结果写入 dp[0] 。

优化后的代码如下。

public class Solution { public static int getLCS(char[] x, char[] y) { // base case,使用滚动数组进行优化 int dp = new int[2][y.length];
// 最长公共子串, 用一个临时变量表示 int resultLCS = 0; for (int i = 1; i < x.length; i++) { for (int j = 1; j < y.length; j++) { // 滚动数组 int cur = (i & 1) == 1 ? 1 : 0; int pre = (i & 1) == 0 ? 1 : 0;
// 状态转移方程 if (x[i] == y[j]) { dp[cur][j] = dp[pre][j-1] + 1; resultLCS = Math.max(resultLCS, dp[cur][j]); } else { dp[cur][j] = 0; } } } return resultLCS; }
public static void main(String[] args) { char x = {' ', 'a', 'b', 'c', 'a', 'd', 'f'}; char y = {' ', 'a', 'c', 'b', 'a', 'd'}; int lcs = Solution.getLCS(x, y); System.out.println("lcs = " + lcs); }}

这样的话我们就将空间复杂度优化成了 O(n)。需要说明的事,利用动态规划的无后效性,通常都可以将空间进行压缩,将空间复杂度降低一个层级,所以大家在做完动态规划的算法题后,还是可以再进一步考虑下问题的复杂度是否还可以继续优化。

4、问题变形

以上我们只是简单求了一下最长公共子串的长度,那如何求其对应的子串呢。还是根据状态状态转移来求,但我们要额外加两个变量 max_row, max_column 代表最长公共子串长度对应的格子的坐标。这样根据状态转移方程可以依次反求得其格子对应的字符,如图示:

代码如下:

public class Solution { public static void printLCS(char[] x, char[] y) { // base case,默认 dp 中的每个元素都为 0。 int dp = new int[x.length][y.length];
// 最长公共子串, 用一个临时变量表示 int resultLCS = 0;
// 记录最长公共子串对应的最后一个格子的模坐标,纵坐标也可以 int maxRow = 0, maxColumn = 0; for (int i = 1; i < x.length; i++) { for (int j = 1; j < y.length; j++) { // 以下是状态转移方程 if (x[i] == y[j]) { dp[i][j] = dp[i-1][j-1] + 1; if (dp[i][j] >= resultLCS) { resultLCS = dp[i][j]; maxRow = i; maxColumn = j; } } else { dp[i][j] = 0; } } }
List<Integer> result = new ArrayList<>; // 根据状态转移方程反推最长公共子序列 while (dp[maxRow][maxColumn] > 0) { result.add(Integer.valueOf(x[maxRow])); maxRow = maxRow-1; maxColumn = maxColumn-1; }
// 算出的最长公共子序列为逆序的,需要反转一下 Collections.reverse(result);
for (Integer item : result) { System.out.print((char) item.intValue); } }
public static void main(String[] args) { char x = {' ', 'a', 'c', 'b', 'a', 'd'}; char y = {' ', 'a', 'b', 'c', 'a', 'd', 'f'}; Solution.printLCS(x, y); }}

总结

本文用图文并茂的方式讲述了动态规划的解题套路,相信大家对字符串类型的动态规划解题技巧应该有了进一步的理解,首先最重要的一步当然是明确 dp 的定义,其实字符串类的动态规划问题一般可以用状态转移表法来观察状态转移方程,得出了状态转移方程,思路也就厘清得八九不离十了,另外当解决完之后,最好思考一下时间/空间复杂度是否有优化的空间,一般可以根据动态规划的无后效性来进一步优化复杂度,通过这样不断地优化自然可以得出问题的最优解。

点分享

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码