动态规划-----最长公共子序列(及其衍生问题)

news2024/12/25 2:04:13

目录

一.最长公共子序列的基本概念:

解决动态规划问题的一般思路(三大步骤):

二.最长公共子序列题目:

三.字符串的删除操作:

四.最小 ASCII 删除和:


一.最长公共子序列的基本概念:

首先需要科普一下,最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢?即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢?给定串中任意个连续的字符组成的子序列称为该串的子串。给一个图再解释一下:

最长公共子序列,顾名思义,就是求两个字符串中子序列的最长的公共部分,返回这个最大的长度,比如说输入 s1 = "zabcde", s2 = "acez",它俩的最长公共子序列是 lcs = "ace",长度为 3,所以算法返回 3。

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

解决动态规划问题的一般思路(三大步骤):

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤:

  • 🧐 步骤一:定义dp数组元素的含义
  • 🧐步骤二:找出数组元素之间的关系式(也就是我们所熟知的状态转移方程)
  • 🧐第三步骤:找出初始值(base case)

接下来的题目我们会按照这三个步骤来解释说明

二.最长公共子序列题目:

计算最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目,力扣第 1143 题「最长公共子序列open in new window」就是这个问题:

对应的函数签名如下:

  • 步骤一:按我上面的步骤说的,首先我们来定义 dp数组的含义,题目要我们求两个字符串的最长公共子序列,给出 对应dp[][] 数组的定义:dp[i][j] 表示串 s1[0..i] 和 s2[0..j] 最长公共子序列的长度
  • 步骤二:找到数组元素之间的关系式(也就是我们所熟知的状态转移方程)

这里咱不要看 s1 和 s2 两个字符串,而是要具体到每一个字符,思考每个字符该做什么:

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

根据dp数组定义可得此时状态转移方程为:dp[ i ][ j ] = 1 + dp[ i - 1 ][ j - 1 ]

②.如果s[i] != s2[j] 意味着,s1[i] 和 s2[j] 中至少有一个字符不在 lcs 中

因为是求最长的公共子序列,所以我们求出对应上述的三种情况的最大值即可,由于情况三被一和二所包(因为我们在求最大值嘛,情况三在计算 s1[i+1..] 和 s2[j+1..] 的 lcs 长度,这个长度肯定是小于等于情况二 s1[i..] 和 s2[j+1..] 中的 lcs 长度的,因为 s1[i+1..] 比 s1[i..] 短嘛,那从这里面算出的 lcs 当然也不可能更长嘛)所以可得:

根据dp数组定义可得此时状态转移方程为:dp[ i ][ j ] = Math.max( dp [ i - 1][ j ],dp[ i ] [ j - 1 ])

  •  步骤三:找出初始值(base case):这里当字符串为空时,没有最大公共子序列,对应的值为0。

我们以ABCB  和 BDCA 为例-----》填dp表:

按照上述的状态转移方程,我们可以将表填完整:

最后,完成上述过程后,动态规划完整代码:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(),n = text2.length();
       // base case: dp[0][..] = dp[..][0] = 0
        int dp[][] = new int[m + 1][n + 1];
        for(int i = 1;i <= m;i++){
            for(int j = 1;j <= n;j++){
                if(text1.charAt(i - 1) == text2.charAt(j - 1)){
                 // text1[i-1] 和 text2[j-1] 必然在 lcs 中
                    dp[i][j] = 1 + dp[i - 1][j -1];
                }else{
                  // text1[i-1] 和 text2[j-1] 至少有一个不在 lcs 中
                    dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

这里还有一种带备忘录的递归式解法,与上面的方法类似:

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

    /* 主函数 */
    public 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];
    }
}

「最长公共子序列」问题基本都是要求返回一个最值即可,但是有时候面试官喜欢不按常理出牌,让你输出最长公共子序列:

我们可以通过构造出来的二维 dp 数组来得到最长公共子序列。如下图所示,从最后一个点开始往左上角的方向遍历 :

如果 s1[i] = s2[j],那么当前字符肯定在最长公共子序列中;否在我们就向左或者向上遍历,至于选择「向左」还是「向上」的方向,这就要和构造 dp 的时候联系起来。我们是挑了一个最大值,所以遍历的方向也是谁大就往谁的方向遍历 ,具体代码:

public static int lcs(String s1,String s2){
       //最长公共子序列框架
        int m = s1.length(),n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for(int i = 1;i <= m;i++){
            for(int j = 1;j <=n;j++){
                if(s1.charAt(i - 1) == s2.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = Math.max(
                            dp[i - 1][j],
                            dp[i][j - 1]
                    );
                }
            }
        }
        //打印最长公共子序列
        int i  = m,j = n;
        StringBuffer sb = new StringBuffer();
        while(i > 0 && j > 0){
            char c1 = s1.charAt(i - 1);
            char c2 = s2.charAt(j - 1);
            if(c1 == c2){
                sb.append(c1);
             // 向左上角遍历
                i--;
                j--;
            }else{
              // 向上
                if(dp[i - 1][j] > dp[i][j - 1]) i--;
               // 向左
                else j--;
            }
        }
      //最后将得到的字符串反转一下,就是我们要的答案了
        System.out.println(sb.reverse());
        return dp[m][n];
    }

有了上面的对最长公共子序列的一定了解,下面,来看两道和最长公共子序列相似的两道题目

三.字符串的删除操作:

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

给定两个单词 s1 和 s2 ,返回使得 s1 和 s2 相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。比如输入 s1 = "sea" s2 = "eat",算法返回 2,第一步将 "sea" 变为 "ea" ,第二步将 "eat" 变为 "ea"

函数签名如下:

题目让我们计算将两个字符串变得相同的最少删除次数,那我们可以思考一下,最后这两个字符串会被删成什么样子?删除的结果不就是它俩的最长公共子序列嘛!那么,要计算删除的次数,就可以通过最长公共子序列的长度推导出来:word1.len - LCS + word2.len - LCS 

与上面的解答类似:

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(),n = word2.length();
        int longest = lcs(word1,word2);
      //推导出的公式
        return m - longest + n - longest;
    }
    int lcs(String s1,String s2){
       //基本最长公共子序列的框架不变
        int m = s1.length(),n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for(int i = 1;i <= m;i++){
            for(int j = 1;j <= n;j++){
                if(s1.charAt(i - 1) == s2.charAt(j - 1)){
                    dp[i][j] = 1 + dp[i - 1][j - 1];
                }else{
                    dp[i][j] = Math.max(
                        dp[i - 1][j],
                        dp[i][j - 1]
                    );
                }
            }
        }
        return dp[m][n];
    }
}

四.最小 ASCII 删除和:

这是力扣第 712 题「两个字符串的最小 ASCII 删除和open in new window」,题目和上一道题目类似,只不过上道题要求删除次数最小化,这道题要求删掉的字符 ASCII 码之和最小化。

对应函数签名:

其实这个题目的底层也是「最长公共子序列」,只是问法稍微变化了一点:

🧐🧐🧐「需要被删除的字符 = 原字符串 - 最长公共子序列」

  • 步骤一:结合这个题目我们把 dp[][] 数组的定义稍微改改:dp[i][j] 表示子串 s1[0..i] 和 s2[0..j] 最小 ASCII 删除和
  • 步骤二:状态转移方程:

①.如果 s1[i] = s2[j]dp[i][j] = dp[i - 1][j - 1] (不需要被删除)

②.如果 s1[i] != s2[j],dp[i][j] = Math.min(dp[i - 1][j] + s1[i], dp[i][j - 1] + s2[j])

  • 步骤三:初始化(base case):

如上图粉色标记出来的就是 base case,e 表示 e 的 ASCII 值

 至此,我们完成了其推导过程,动态规划解法代码:

class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(),n = s2.length();
      //创建dp表
        int[][] dp = new int[m + 1][n + 1];
      //初始化dp表
        dp[0][0] = 0;
        for(int i = 1;i <= m;i++){
            dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
        }
        for(int j = 1;j <= n;j++){
            dp[0][j] = dp[0][j - 1] + s2.charAt(j - 1);
        }
        //填表
        for(int i = 1;i <= m;i++){
           for(int j = 1;j <= n;j++){
            //相等情况
            if(s1.charAt(i - 1) == s2.charAt(j - 1)){
                dp[i][j] = dp[i - 1][j - 1];
            }else{
                //不相等情况
                dp[i][j] = Math.min(
                   s1.charAt(i - 1) + dp[i - 1][j],
                   s2.charAt(j - 1) + dp[i][j - 1]
                );
            }
           }
        }
      //返回值
        return dp[m][n];
    }
}

参考文章:《labuladong的算法笔记》,告别动态规划,连刷40道动规算法题,我总结了动规的套路-CSDN博客

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固自己的知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+收藏+关注,你们的鼓励是我创作的最大动力!

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

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

相关文章

微前端——qiankun

一、微前端 微前端是指存在于浏览器中的微服务&#xff0c;其借鉴了后端微服务的架构理念&#xff0c;将微服务的概念扩展到前端。即将一个大型的前端应用拆分为成多个模块&#xff0c;每个微前端模块可以有不同的团队开发并进行管理&#xff0c;且可以自主选择框架&#xff0…

seata测试demo(订单)

seata工作流程: seata对分布式事务的协调和控制就是31 1>XID&#xff1a;XID是全局事务的唯一标识&#xff0c;它可以在服务的调用链路中传递&#xff0c;绑定到服务的事务上下文中。 3>TC->TM->RM TC:事务协调器>就是seata 负责维护全局事务和分支事务的状…

选项式API和组合式API的区别

选项式(options) API 和组合式(composition) API两种不同的风格书写&#xff0c;Vue3 的组件可以使用这两种api来编写。 选项式API和组合式API的区别 选项式API 选项式 API&#xff0c;具有相同功能的放在一起&#xff0c;可以用包含多个选项的对象来描述组件的逻辑&…

【周总结】

周总结 完成项目混合版时区改造 完成相关jira问题的修改 完成老版本APP数据保存接口的兼容&#xff0c;手动赋值时区 2024/03/24 天气阴 一点不冷 1.Its time to go、Spring is coming&#xff01; 2. Its a nice day that staying with friends in a peaceful …

初探Ruby编程语言

文章目录 引言一、Ruby简史二、Ruby特性三、安装Ruby四、命令行执行Ruby五、Ruby的编程模型六、案例演示结语 引言 大家好&#xff0c;今天我们将一起探索一门历史悠久、充满魅力的编程语言——Ruby。Ruby是由松本行弘&#xff08;Yukihiro Matsumoto&#xff09;于1993年发明…

LangChain核心模块 Retrieval——文档加载器

Retrieval ​ 许多LLM申请需要用户的特定数据&#xff0c;这些数据不属于模型训练集的一部分&#xff0c;实现这一目标的主要方法是RAG(检索增强生成)&#xff0c;在这个过程中&#xff0c;将检索外部数据&#xff0c;然后在执行生成步骤时将其传递给LLM。 ​ LangChain 提供…

Linux系统安装openGauss结合内网穿透实现公网访问本地数据库管理系统——“cpolar内网穿透”

文章目录 前言1. Linux 安装 openGauss2. Linux 安装cpolar3. 创建openGauss主节点端口号公网地址4. 远程连接openGauss5. 固定连接TCP公网地址6. 固定地址连接测试 前言 openGauss是一款开源关系型数据库管理系统&#xff0c;采用木兰宽松许可证v2发行。openGauss内核深度融合…

力扣:205. 同构字符串

前言&#xff1a;剑指offer刷题系列 问题&#xff1a; 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符…

Linux快速入门,上手开发 02.VMware的安装部署

倘若穷途末路&#xff0c;那便势如破竹 —— 24.3.21 一、VMware的作用 在Windows或IOS系统下&#xff0c;给本地电脑安装VMware虚拟机&#xff0c;用来在虚拟机上安装Linux系统&#xff0c;避免重复资源的浪费&#xff0c;可以在虚拟机上搭建Linux系统进行学习 二、VMware的安…

分布式数据库TiDB介绍及基本原理

1.概述&#xff1a; 1.1 标准SQL、noSQL、newSQL的区别&#xff1a; SQL(Structured Query Language)&#xff1a;数据库&#xff0c;指传统的关系型数据库。缺点是面对大量的数据时&#xff0c;他的性能会随着数据库的增大而急剧下降。主要代表&#xff1a;SQL Server、Orac…

Data.olllo:轻松统计分类总数!

介绍&#xff1a; Data.olllo是您数据处理的得力助手&#xff0c;拥有众多强大的功能&#xff0c;其中之一便是“分类总数”功能。这个功能能够帮助您快速准确地统计某一列中不同分类的总数&#xff0c;无论是分类为A、B、C&#xff0c;还是其他自定义分类&#xff0c;都能轻松…

进行信创符合性检测是什么意思?

验收文件中&#xff0c;要求“进行信创符合性检测”&#xff0c;这些检测包括什么内容&#xff0c;需要提供什么证明材料&#xff1f; 这个问题相对复杂一些。首先&#xff0c;我们需要了解什么是“信创符合性”。大家都清楚&#xff0c;信创行业发展&#xff0c;是关系到国家…

无需敲代码,10s一个网页

无需掌握前端三剑客的知识&#xff0c;10s种做出下图的效果。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">&…

群晖NAS使用docker安装容器魔方结合内网穿透实现公网访问

文章目录 1. 拉取容器魔方镜像2. 运行容器魔方3. 本地访问容器魔方4. 群辉安装Cpolar5. 配置容器魔方远程地址6. 远程访问测试7. 固定公网地址 本文主要介绍如何在群辉7.2版本中使用Docker安装容器魔方&#xff0c;并结合Cpolar内网穿透工具实现远程访问本地网心云容器魔方界面…

Redis入门到实战-第八弹

Redis实战热身Sorted sets篇 完整命令参考官网 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://redis.io/Redis概述 Redis是一个开源的&#xff08;采用BSD许可证&#xff09;&#xff0c;用作数据库、缓存、消息代…

React Native 应用打包上架

引言 在将React Native应用上架至App Store时&#xff0c;除了通常的上架流程外&#xff0c;还需考虑一些额外的优化策略。本文将介绍如何通过配置App Transport Security、Release Scheme和启动屏优化技巧来提升React Native应用的上架质量和用户体验。 配置 App Transport…

CV论文--2024.3.25

1、Zero-Shot Multi-Object Shape Completion 中文标题&#xff1a;零样本多对象形状完成 简介&#xff1a;我们提出了一种3D形状补全方法&#xff0c;可以从单个RGB-D图像中恢复复杂场景中多个物体的完整几何形状。尽管单个物体的3D形状补全已经取得了显著进展&#xff0c;但…

Oracle:ORA-01830错误-更改数据库时间格式

1,先把报错SQL语句拿出来执行&#xff0c;看看是不是报的这个错 ORA-01830: 日期格式图片在转换整个输入字符串之前结束 2&#xff0c;然后查看默认日期格式是不是“YYYY-MM-DD HH24:MI:SS”&#xff08;正确格式&#xff09;。&#xff1b; 执行&#xff1a; SELECT * FRO…

用three.js做一个3D汉诺塔游戏(上)

本文由孟智强同学原创&#xff0c;主要介绍了如何利用 three.js 开发 3D 应用&#xff0c;涵盖 3D 场景搭建、透视相机、几何体、材质、光源、3D 坐标计算、补间动画以及物体交互实现等知识点。 入门 three.js 也有一阵子了&#xff0c;我发现用它做 3D 挺有趣的&#xff0c;而…

unity 学习笔记 4.坐标系

下载源码 UnityPackage 目录 1.基础知识 1.1.世界坐标和局部坐标 1.2.屏幕坐标 2.坐标系转换 3.练习&#xff1a;判断鼠标单击的位置 1.基础知识 1.1.世界坐标和局部坐标 1.2.屏幕坐标 2.坐标系转换 3.练习&#xff1a;判断鼠标单击的位置 步骤&#xff1a; 将脚本挂载到小…