vb treeview 展开子节点_详解最长公共子序列问题,秒杀三道动态规划题目

学算法认准 labuladong

后台回复进群一起力扣?

051d45acbf47159998dd1b19b8428b93.png

读完本文,可以去力扣解决如下题目:

1143.最长公共子序列(Medium)

583. 两个字符串的删除操作(Medium)

712.两个字符串的最小ASCII删除和(Medium)

051d45acbf47159998dd1b19b8428b93.png

好久没写动态规划算法相关的文章了,今天来搞一把。

不知道大家做算法题有什么感觉,我总结出来做算法题的技巧就是,把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题

比如说我们前文 手把手带你刷二叉树第三期,解决二叉树的题目,我们就会把整个问题细化到某一个节点上,想象自己站在某个节点上,需要做什么,然后套二叉树递归框架就行了。

动态规划系列问题也是一样,尤其是子序列相关的问题。本文从「最长公共子序列问题」展开,总结三道子序列问题,解这道题仔细讲讲这种子序列问题的套路,你就能感受到这种思维方式了。

最长公共子序列

计算最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目,大家应该都见过:

给你输入两个字符串s1s2,请你找出他们俩的最长公共子序列,返回这个子序列的长度。

力扣第 1143 题就是这道题,函数签名如下:

int longestCommonSubsequence(String s1, String s2);

比如说输入s1 = "zabcde", s2 = "acez",它俩的最长公共子序列是lcs = "ace",长度为 3,所以算法返回 3。

如果没有做过这道题,一个最简单的暴力算法就是,把s1s2的所有子序列都穷举出来,然后看看有没有公共的,然后在所有公共子序列里面再寻找一个长度最大的。

显然,这种思路的复杂度非常高,你要穷举出所有子序列,这个复杂度就是指数级的,肯定不实际。

正确的思路是不要考虑整个字符串,而是细化到s1s2的每个字符。前文 子序列解题模板 中总结的一个规律:

对于两个字符串求子序列的问题,都是用两个指针ij分别在两个字符串上移动,大概率是动态规划思路

最长公共子序列的问题也可以遵循这个规律,我们可以先写一个dp函数:

// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j)

这个dp函数的定义是:dp(s1, i, s2, j)计算s1[i..]s2[j..]的最长公共子序列长度

根据这个定义,那么我们想要的答案就是dp(s1, 0, s2, 0),且 base case 就是i == len(s1)j == len(s2)时,因为这时候s1[i..]s2[j..]就相当于空串了,最长公共子序列的长度显然是 0:

int longestCommonSubsequence(String s1, String s2) {
    return dp(s1, 0, s2, 0);
}

/* 主函数 */
int dp(String s1, int i, String s2, int j) {
    // base case
    if (i == s1.length() || j == s2.length()) {
        return 0;
    }
    // ...

接下来,咱不要看s1s2两个字符串,而是要具体到每一个字符,思考每个字符该做什么

5be5489422a87fac8d7f6b73c2836bf9.png

我们只看s1[i]s2[j]如果s1[i] == s2[j],说明这个字符一定在lcs

947ddc1e784bb73548e77c0768eb771a.png

这样,就找到了一个lcs中的字符,根据dp函数的定义,我们可以完善一下代码:

// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
    if (s1.charAt(i) == s2.charAt(j)) {
        // s1[i] 和 s2[j] 必然在 lcs 中,
        // 加上 s1[i+1..] 和 s2[j+1..] 中的 lcs 长度,就是答案
        return 1 + dp(s1, i + 1, s2, j + 1)
    } else {
        // ...
    }
}

刚才说的s1[i] == s2[j]的情况,但如果s1[i] != s2[j],应该怎么办呢?

s1[i] != s2[j]意味着,s1[i]s2[j]中至少有一个字符不在lcs

09ac2b178476ec114434544e3b2c6a3d.png

如上图,总共可能有三种情况,我怎么知道具体是那种情况呢?

其实我们也不知道,那就把这三种情况的答案都算出来,取其中结果最大的那个呗,因为题目让我们算「最长」公共子序列的长度嘛。

这三种情况的答案怎么算?回想一下我们的dp函数定义,不就是专门为了计算它们而设计的嘛!

代码可以再进一步:

// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
    if (s1.charAt(i) == s2.charAt(j)) {
        return 1 + dp(s1, i + 1, s2, j + 1)
    } else {
        // s1[i] 和 s2[j] 中至少有一个字符不在 lcs 中,
        // 穷举三种情况的结果,取其中的最大结果
        return max(
            // 情况一、s1[i] 不在 lcs 中
            dp(s1, i + 1, s2, j),
            // 情况二、s2[j] 不在 lcs 中
            dp(s1, i, s2, j + 1),
            // 情况三、都不在 lcs 中
            dp(s1, i + 1, s2, j + 1)
        );
    }
}

这里就已经非常接近我们的最终答案了,还有一个小的优化,情况三「s1[i]s2[j]都不在 lcs 中」其实可以直接忽略

因为我们在求最大值嘛,情况三在计算s1[i+1..]s2[j+1..]lcs长度,这个长度肯定是小于等于情况二s1[i..]s2[j+1..]中的lcs长度的,因为s1[i+1..]s1[i..]短嘛,那从这里面算出的lcs当然也不可能更长嘛。

同理,情况三的结果肯定也小于等于情况一。说白了,情况三被情况一和情况二包含了,所以我们可以直接忽略掉情况三,完整代码如下:

// 备忘录,消除重叠子问题
int[][] memo;

/* 主函数 */
int longestCommonSubsequence(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 备忘录值为 -1 代表未曾计算
    memo = new int[m][n];
    for (int[] row : memo) 
        Arrays.fill(row, -1);
    // 计算 s1[0..] 和 s2[0..] 的 lcs 长度
    return dp(s1, 0, s2, 0);
}

// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
    // base case
    if (i == s1.length() || j == s2.length()) {
        return 0;
    }
    // 如果之前计算过,则直接返回备忘录中的答案
    if (memo[i][j] != -1) {
        return memo[i][j];
    }
    // 根据 s1[i] 和 s2[j] 的情况做选择
    if (s1.charAt(i) == s2.charAt(j)) {
        // s1[i] 和 s2[j] 必然在 lcs 中
        memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1);
    } else {
        // s1[i] 和 s2[j] 至少有一个不在 lcs 中
        memo[i][j] = Math.max(
            dp(s1, i + 1, s2, j),
            dp(s1, i, s2, j + 1)
        );
    }
    return memo[i][j];
}

以上思路完全就是按照我们之前的爆文 动态规划套路框架 来的,应该是很容易理解的。至于为什么要加memo备忘录,我们之前写过很多次,为了照顾新来的读者,这里再简单重复一下,首先抽象出我们核心dp函数的递归框架:

int dp(int i, int j) {
    dp(i + 1, j + 1); // #1
    dp(i, j + 1);     // #2
    dp(i + 1, j);     // #3
}

你看,假设我想从dp(i, j)转移到dp(i+1, j+1),有不止一种方式,可以直接走#1,也可以走#2 -> #3,也可以走#3 -> #2

这就是重叠子问题,如果我们不用memo备忘录消除子问题,那么dp(i+1, j+1)就会被多次计算,这是没有必要的。

至此,最长公共子序列问题就完全解决了,用的是自顶向下带备忘录的动态规划思路,我们当然也可以使用自底向上的迭代的动态规划思路,和我们的递归思路一样,关键是如何定义dp数组,我这里也写一下自底向上的解法吧:

int longestCommonSubsequence(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
    // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
    // base case: dp[0][..] = dp[..][0] = 0

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 现在 i 和 j 从 1 开始,所以要减一
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                // s1[i-1] 和 s2[j-1] 必然在 lcs 中
                dp[i][j] = 1 + dp[i - 1][j - 1];
            } else {
                // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
                dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
    }

    return dp[m][n];
}

自底向上的解法中dp数组定义的方式和我们的递归解法有一点差异,而且由于数组索引从 0 开始,有索引偏移,不过思路和我们的递归解法完全相同,如果你看懂了递归解法,这个解法应该不难理解。

另外,自底向上的解法可以通过我们前文讲过的 动态规划状态压缩技巧 来进行优化,把空间复杂度压缩为 O(N),这里由于篇幅所限,就不展开了。

下面,来看两道和最长公共子序列相似的两道题目。

字符串的删除操作

这是力扣第 583 题「两个字符串的删除操作」,看下题目:

ab654bc5f8329d23610cb61301552803.png

函数签名如下:

int minDistance(String s1, String s2);

题目让我们计算将两个字符串变得相同的最少删除次数,那我们可以思考一下,最后这两个字符串会被删成什么样子?

删除的结果不就是它俩的最长公共子序列嘛!

那么,要计算删除的次数,就可以通过最长公共子序列的长度推导出来:

int minDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 复用前文计算 lcs 长度的函数
    int lcs = longestCommonSubsequence(s1, s2);
    return m - lcs + n - lcs;
}

这道题就解决了!

最小 ASCII 删除和

这是力扣第 712 题,看下题目:

461d3ac2f24766e86adf0d5f0a8a98f1.png

这道题,和上一道题非常类似,这回不问我们删除的字符个数了,问我们删除的字符的 ASCII 码加起来是多少。

那就不能直接复用计算最长公共子序列的函数了,但是可以依照之前的思路,稍微修改 base case 和状态转移部分即可直接写出解法代码

// 备忘录
int memo[][];
/* 主函数 */    
int minimumDeleteSum(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 备忘录值为 -1 代表未曾计算
    memo = new int[m][n];
    for (int[] row : memo) 
        Arrays.fill(row, -1);

    return dp(s1, 0, s2, 0);
}

// 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串,
// 最小的 ASCII 码之和为 dp(s1, i, s2, j)。
int dp(String s1, int i, String s2, int j) {
    int res = 0;
    // base case
    if (i == s1.length()) {
        // 如果 s1 到头了,那么 s2 剩下的都得删除
        for (; j             res += s2.charAt(j);
        return res;
    }
    if (j == s2.length()) {
        // 如果 s2 到头了,那么 s1 剩下的都得删除
        for (; i             res += s1.charAt(i);
        return res;
    }

    if (memo[i][j] != -1) {
        return memo[i][j];
    }

    if (s1.charAt(i) == s2.charAt(j)) {
        // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除
        memo[i][j] = dp(s1, i + 1, s2, j + 1);
    } else {
        // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个
        memo[i][j] = Math.min(
            s1.charAt(i) + dp(s1, i + 1, s2, j),
            s2.charAt(j) + dp(s1, i, s2, j + 1)
        );
    }
    return memo[i][j];
}

base case 有一定区别,计算lcs长度时,如果一个字符串为空,那么lcs长度必然是 0;但是这道题如果一个字符串为空,另一个字符串必然要被全部删除,所以需要计算另一个字符串所有字符的 ASCII 码之和。

关于状态转移,当s1[i]s2[j]相同时不需要删除,不同时需要删除,所以可以利用dp函数计算两种情况,得出最优的结果。其他的大同小异,就不具体展开了。

至此,三道子序列问题就解决完了,关键在于将问题细化到字符,根据每两个字符是否相同来判断他们是否在结果子序列中,从而避免了对所有子序列进行穷举。

这也算是在两个字符串中求子序列的常用思路吧,建议好好体会,多多联系~

往期推荐 ?

东哥手把手带你套框架刷通二叉树|第一期

阶乘相关的算法题,东哥又整活儿了

东哥手写正则通配符算法,结构清晰,包教包会!

关于算法笔试,东哥又整出套路了?

原创 | 东哥教你几招常用的位运算技巧

_____________

学好算法靠套路,认准 labuladong,知乎、B站账号同名。

《labuladong的算法小抄》即将出版,公众号后台回复关键词「pdf」下载,回复「进群」可加入刷题群。

edb231a7e3e7351e57f5147a4b34d4b7.png

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

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

相关文章

TZOJ 4621 Grammar(STL模拟)

描述 Our strings only contain letters(maybe the string contains nothing). Now we define the production as follows: 1. (C) --> C 2. C --> C 3. (C:num)-->repeat C num times. Illustration: (C) or C stands for a string only contains letters. (C:num) m…

[链接]Python中的metaclass、装饰器

深刻理解Python中的元类(metaclass) Python装饰器学习&#xff08;九步入门&#xff09;

linux查看数据积压,查看kafka消息队列的积压情况

创建topickafka-topics --create --zookeeper master:2181/kafka2 --replication-factor 2 --partitions 3 --topic mydemo5列出topickafka-topics --list --zookeeper master:2181/kafka2描述topickafka-topics --describe --zookeeper master:2181/kafka2 --topic mydemo5生产…

全国计算机等级考试题库二级C操作题100套(第84套)

第84套&#xff1a; 函数fun的功能是&#xff1a;从三个形参a&#xff0c;b&#xff0c;c中找出中间的那个数&#xff0c;作为函数值返 回。 例如&#xff0c;当a3, b5, c4时&#xff0c;中数为4。 请在程序的下划线处填入正确的内容并把下划线删除,使程序得出正确的结果。 注…

python 三引号_Python 基础(一):入门必备知识

目录1 标识符2 关键字3 引号4 编码5 输入输出6 缩进7 多行8 注释9 数据类型10 运算符10.1 常用运算符10.2 运算符优先级1 标识符标识符是编程时使用的名字&#xff0c;用于给变量、函数、语句块等命名&#xff0c;Python 中标识符由字母、数字、下划线组成&#xff0c;不能以数…

排序算法:冒泡和快排 摘自网络

冒泡排序&#xff1a; 首先我们自己来设计一下“冒泡排序”&#xff0c;这种排序很现实的例子就是&#xff1a; 我抓一把沙仍进水里&#xff0c;那么沙子会立马沉入水底&#xff0c; 沙子上的灰尘会因为惯性暂时沉入水底&#xff0c;但是又会立马像气泡一样浮出水面&#xff0c…

镭波笔记本安装linux,镭波笔记本windows7旗舰版系统下载与安装教程

镭波笔记本windows7旗舰版系统下载地址以及安装教程有很多盆友询问&#xff0c;今天&#xff0c;我就将镭波电脑下载安装win7旗舰版系统的详细步骤分享给你们,一起来了解一下镭波电脑是如何安装windows7旗舰版。镭波笔记本Windows7旗舰版系统下载&#xff1a;64位Windows7旗舰版…

全国计算机等级考试题库二级C操作题100套(第85套)

第85套&#xff1a; 给定程序的功能是调用fun函数建立班级通讯录。通讯录中记录每位学生的编号、姓名和电话号码。班级的人数和学生的信息从键盘读入,每个人的信息作为一个数据块写到名为myfile5.dat的二进制文件中。 请在程序的下划线处填入正确的内容并把下划线删除&#xf…

使用泛型解决之前的问题

package fanxing; /* * 使用泛型解决之前的问题 */ import java.util.ArrayList;import java.util.Iterator;import java.util.LinkedList; public class TestGeneric02 { public static void main(String[] args) { LinkedList<Integer> list new LinkedList<>()…

python中文姓名排序_Python实现针对中文排序的方法

本文实例讲述了Python实现针对中文排序的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;Python比较字符串大小时&#xff0c;根据的是ord函数得到的编码值。基于它的排序函数sort可以很容易为数字和英文字母排序&#xff0c;因为它们在编码表中就是顺序排列的。但…

网站数据库中“密码加密”方法思考

博主&#xff0c;开始时候&#xff0c;写的web应用&#xff0c;数据库中的密码&#xff0c;都是直接保存的。真正的原汁原味&#xff0c;真正的所见即所得。 后来&#xff0c;看了别人的web应用都是MD5加密的&#xff0c;感觉很不错&#xff0c;自己也在应用中加入了MD5加密&am…

linux运维和3dmax哪个简单,牛逼运维常用的工具系列-2

劳动最光荣nmonnmon是linux性能监视和分析数据的工具&#xff0c;它的安装很简单&#xff0c;下载解压后&#xff0c;添加可执行权限&#xff0c;即可运行下载解压后&#xff0c;通过文件名可以发现&#xff0c;是多个发行版本的&#xff0c;根据自己的发行版本&#xff0c;然后…

全国计算机等级考试题库二级C操作题100套(第86套)

第86套&#xff1a; 甲乙丙丁四人同时开始放鞭炮&#xff0c;甲每隔t1秒放一次&#xff0c;乙每隔t2秒放一次, 丙 每隔t3秒放一次&#xff0c;丁每隔t4秒放一次&#xff0c;每人各放n次。函数fun的功能是根据形参 提供的值&#xff0c;求出总共听到多少次鞭炮声作为函数值返回…

语义分割和实例分割_语义分割入门的一点总结

点击上方“CVer”&#xff0c;选择加"星标"或“置顶”重磅干货&#xff0c;第一时间送达作者&#xff1a;Yanpeng Sunhttps://zhuanlan.zhihu.com/p/74318967本文已由作者授权&#xff0c;未经允许&#xff0c;不得二次转载语义分割目的&#xff1a;给定一张图像&…

【转载】什么是C++虚函数、虚函数的作用和使用方法

我们知道&#xff0c;在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的&#xff0c;否则就是“重复定义”。但是在类的继承层次结构中&#xff0c;在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。例如在例12.1&#xff08;具体代码请…

linux 音频驱动的流程,Intel平台下Linux音频驱动流程分析

【软件框架】在对要做的事情一无所知的时候&#xff0c;从全局看看系统的拓扑图对我们认识新事物有很大的帮助。Audio 部分的驱动程序框架如下图所示&#xff1a;这幅图明显地分为 3 级。上方蓝色系的 ALSA Kernel 整体属于Linux Kernel&#xff0c;是原生Linux 操作系统的一部…

Windows Server 2008 R2Cisco2960 配置Radius服务 实现802.1x认证 实战

实战配置Windows Server 2008 R2 Radius服务 与Cisco 2960 实现 802.1x认证实验拓扑1.Radius服务器 安装 dc 域名 wjl.com &#xff0c;和ca 安装步骤不再详解2.安装完ca之后&#xff0c;打开MMC 添加计算机证书&#xff0c;查看个人-证书里面有没有ca颁发给计算机的证书&…

python多线程下载器_用 python 实现一个多线程网页下载器

学习之#!/usr/bin/env python# -*- coding:utf-8 -*-import urllib, httplibimport threadimport timefrom Queue import Queue, Empty, FullHEADERS {"Content-type": "application/x-www-form-urlencoded",Accept-Language:zh-cn,User-Agent: Mozilla/4…

全国计算机等级考试题库二级C操作题100套(第87套)

第87套&#xff1a; 函数fun的功能是&#xff1a;统计长整数n的各个位上出现数字1、2、3的次数,并通过外部(全局)变量c1&#xff0c;c2&#xff0c;c3返回主函数。例如&#xff1a;当n123114350时&#xff0c;结果应该为: c13 c21 c32。 请在程序的下划线处填入正确的内容并把…

Linux命令之ifconfig

ifconfig [interface] ifconfig interface [aftype] options | address… ifconfig是用来查看和配置网络接口的工具。Ifconfig配置的信息在网络设备重启后&#xff0c;配置就会还原。需要永久保存&#xff0c;请进入配置文件配置。 address可以是inet&#xff08;TCP/IP&#x…