【动态规划】两个数组 / 字符串的dp问题(子序列、子数组问题、匹配问题、字符串问题)

news2024/9/24 15:20:55

文章目录

  • 前言
  • 算法题
    • 1.最长公共子序列
    • 2.不相交的线
    • 3.不同的子序列
    • 4.通配符匹配
    • 5.正则表达式匹配
    • 6.交错字符串
    • 7.两个字符串的最小ASCII删除和
    • 8.最长重复子数组

前言

两个数组或字符串的动态规划问题通常涉及到比较和匹配元素。以下是两个常见的例子:

  1. 最长公共子序列 (LCS) 问题

问题描述
给定两个字符串 s1s2,找出它们的最长公共子序列的长度。

  1. 编辑距离问题

问题描述
给定两个字符串 word1word2,计算将 word1 转换为 word2 所需的最小操作次数(插入、删除、替换)。


算法题

1.最长公共子序列

在这里插入图片描述

思路

  1. 定义状态

    • 用一个二维数组 dp 来存储状态,其中 dp[i][j] 代表字符串 text1 的前 i 个字符和字符串 text2 的前 j 个字符的最长公共子序列的长度。
  2. 状态转移

    • 如果 text1[i-1] 等于 text2[j-1],则 dp[i][j]dp[i-1][j-1] 的值加 1。
    • 如果不等,dp[i][j]dp[i-1][j]dp[i][j-1] 中的较大值。
  3. 初始化

    • dp 数组初始化为 0。
  4. 结果

    • 最终的结果是 dp[m][n],即两个字符串的最长公共子序列的长度。

代码

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        // 创建+初始化dp数组
        // dp[i][j]:在s1串 的{0, 1}范围 和 s2串的{0, j}范围的最长公共子序列
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        // 填表
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++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[m][n];
    }
};

2.不相交的线

在这里插入图片描述

思路

这道题实际上就是上一题的变体,也就是求最长公共子序列。

代码

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        // 即[1143.最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/description/)
        int m = nums1.size(), n = nums2.size();
        // 创建 + 初始化dp数组
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));

        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                if(nums1[i - 1] == nums2[j - 1]) // 下标映射
                    dp[i][j] = dp[i-1][j-1] + 1;
                else
                    dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
            }

        return dp[m][n];
    }
};

3.不同的子序列

在这里插入图片描述

思路

  1. 状态定义dp[i][j] 表示字符串 s 的前 j 个字符中,t 的前 i 个字符作为子序列的数量。

  2. 初始化dp[0][j] = 1,表示 t 是空字符串时,任何 s 的前 j 个字符中都有 1 种不同的子序列(即空子序列)。

  3. 状态转移

    • dp[i][j] += dp[i][j-1]:在当前字符 s[j-1] 不包含在子序列中的情况下的数量。
    • 如果 t[i-1] == s[j-1],则 dp[i][j] += dp[i-1][j-1],表示包含当前字符 s[j-1] 时的数量。
  4. 结果dp[m][n],表示 s 中包含 t 的所有不同子序列的数量。

  • 时间复杂度 O(m * n)
  • 空间复杂度 O(m * n)

代码

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = t.size(), n = s.size();
        // 创建+初始化 dp数组
        vector<vector<int>> dp(m+1, vector<int>(n+1));
        for(int j = 0; j <= n; ++j) dp[0][j] = 1;

        // 填表
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                dp[i][j] += dp[i][j-1];
                if(t[i-1] == s[j-1]) dp[i][j] += dp[i-1][j-1];
            }

        return dp[m][n];
    }
};

4.通配符匹配

在这里插入图片描述

思路

  1. 状态定义

    • dp[i][j] 表示字符串 s 的前 i 个字符是否可以被模式 p 的前 j 个字符匹配。
  2. 初始化

    • dp[0][0] = true:空字符串和空模式是匹配的。
    • 对于模式中的 *dp[0][j] 需要设置为 true,因为 * 可以匹配零个字符。初始化时,从模式的开头开始,连续的 * 会让 dp[0][j] 设为 true
  3. 状态转移

    • 当模式字符 p[j]* 时,dp[i][j] 可以由以下两种情况获得:
      • dp[i-1][j]:表示模式 p* 匹配了 s 的当前字符,并且剩余的 s 部分可以继续匹配模式。
      • dp[i][j-1]:表示模式 p* 匹配了零个字符,即模式的 * 前面的部分与 s 的当前部分匹配。
    • 当模式字符 p[j] 不是 * 时,dp[i][j] 的值取决于当前模式字符是否等于 s 的当前字符,或者模式字符是否为 ?。如果是,则 dp[i][j] 可以由 dp[i-1][j-1] 获得。
  4. 结果

    • 最终的结果是 dp[m][n],表示整个字符串 s 是否可以被模式 p 匹配。
  • 时间复杂度O(m * n),因为我们需要填充 m * ndp 表。
  • 空间复杂度O(m * n),使用了一个 m+1 行、n+1 列的二维数组来存储中间结果。

代码

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        // 创建dp数组
        vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
        // 初始化虚拟空间
        dp[0][0] = true;
        for(int j = 1; j <= n; ++j)
            if(p[j] == '*') dp[0][j] = true;
            else break;

        // 填表
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                if(p[j] == '*') {
                    dp[i][j] = dp[i-1][j] || dp[i][j-1];
                }   
                else {
                    dp[i][j] = (p[j] == '?' || p[j] == s[i]) && dp[i-1][j-1];
                }
            }

        return dp[m][n];
    }
};

5.正则表达式匹配

在这里插入图片描述

思路

  1. 初始化

    • 在字符串 s 和模式 p 前面各加一个空格(即使 sp 从索引 1 开始),这样可以简化下标操作,使 s[i]p[j] 对应的下标与 dp 表一致。
    • 创建一个二维布尔数组 dp,其中 dp[i][j] 表示字符串 s 的前 i 个字符是否与模式 p 的前 j 个字符匹配。
  2. 处理模式的初始化

    • 初始化 dp[0][j],这表示空字符串与模式 p 的前 j 个字符的匹配。因为 * 可以匹配零个或多个字符,所以当模式的第 j 个字符是 * 时,我们需要检查 dp[0][j-2](即模式中 * 前面的字符可能不出现的情况)。
  3. 填表

    • 对于每个字符 s[i]p[j],检查模式 p 当前字符是 * 还是普通字符。
    • 如果是 *,有两个匹配条件:
      • dp[i][j-2]:表示 * 匹配零个字符。
      • (p[j-1] == '.' || p[j-1] == s[i]) && dp[i-1][j]:表示 * 匹配一个或多个字符。
    • 如果是普通字符,检查 p[j] 是否与 s[i] 匹配,或者 p[j] 是否为 .,然后根据 dp[i-1][j-1] 更新 dp[i][j]
  4. 返回结果

    • 最终的匹配结果保存在 dp[m][n] 中。
  • 时间复杂度O(m * n)。由于我们使用了一个 m+1 行和 n+1 列的二维 DP 表,且每个元素的计算都需要常量时间,所以总体时间复杂度是 O(m * n)

  • 空间复杂度O(m * n)。我们使用了一个 m+1 行和 n+1 列的二维 DP 表来存储中间结果。因此,空间复杂度为 O(m * n)

代码

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        // 创建dp表
        vector<vector<bool>> dp(m+1, vector<bool>(n+1,false));
        // 初始化dp表
        dp[0][0] = true;
        for(int j = 2; j <= n; j+=2)
            if(p[j] == '*') dp[0][j] = true;
            else break;

        // 填表
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                if(p[j] == '*')
                    dp[i][j] = dp[i][j-2] || (p[j-1] == '.' || p[j-1] == s[i]) && dp[i-1][j];
                else
                    dp[i][j] = dp[i-1][j-1] && (p[j] == s[i] || p[j] == '.');
            }

        return dp[m][n];
    }
};

6.交错字符串

在这里插入图片描述

思路

  1. 初始化

    • 创建一个二维 DP 数组 dp[i][j],其中 dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符是否能交错组成 s3 的前 i+j 个字符。
    • dp[0][0] 初始化为 true,表示空字符串可以交错合成空字符串。
    • dp[0][j]dp[i][0] 进行初始化,分别处理当 s1s2 为空的情况。
  2. 填表

    • 遍历 DP 表,更新每个 dp[i][j],根据 s1[i-1]s2[j-1] 是否等于 s3[i+j-1] 来决定是否更新 dp[i][j]

时空复杂度

  • 时间复杂度O(m * n),其中 ms1 的长度,ns2 的长度。填充 DP 表需要遍历所有 m * n 的位置。
  • 空间复杂度O(m * n),需要一个大小为 m * n 的 DP 表。

代码

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int m = s1.size(), n = s2.size();
        if(m + n != s3.size()) return false;
        // 预处理:加占位符对应下标
        s1 = " " + s1, s2 = " " + s2, s3 = " " + s3;
        // 创建dp数组
        // dp[i][j]: s1{1, i}区间 与 s2[1, j]区间是否能匹配s3{1, i+j}区间
        vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
        // 初始化
        dp[0][0] = true;
        for(int j = 1; j <= n; ++j) // 初始化第一行
            if(s2[j] == s3[j]) dp[0][j] = true;
            else break;
        for(int i = 1; i <= m; ++i) // 初始化第一列
            if(s1[i] == s3[i]) dp[i][0] = true;
            else break;

        // 填表
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                dp[i][j] = (s2[j] == s3[i+j] && dp[i][j-1])
                            ||(s1[i] == s3[i+j] && dp[i-1][j]);
            }

        return dp[m][n];
    }
};

7.两个字符串的最小ASCII删除和

在这里插入图片描述

思路

  1. 定义状态

    • dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符的公共子序列的最大ASCII值之和。
  2. 初始化和状态转移

    • 初始化 dp[i][0]dp[0][j] 为0,因为一个字符串为空时,公共子序列的ASCII和为0。
    • 对于每对 (i, j),比较 s1[i-1]s2[j-1]
      • 如果它们相同,则 dp[i][j]dp[i-1][j-1] + s1[i-1]
      • 否则,dp[i][j]max(dp[i-1][j], dp[i][j-1])
  3. 计算结果

    • 计算所有字符的ASCII和,并从中减去两次 dp[m][n],因为每个字符被计算了两次。

时空复杂度

  • 时间复杂度O(m * n),填充 DP 表需要遍历所有 m * n 的位置。
  • 空间复杂度O(m * n),需要一个大小为 m * n 的 DP 表。

代码

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int m = s1.size(), n = s2.size();
        vector<vector<int>> dp(m+1, vector<int>(n+1));

        // 正难则反:求公共子序列的最大ASCII值
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                if(s1[i-1] == s2[j-1])
                    dp[i][j] = max(dp[i][j], dp[i-1][j-1] + s1[i-1]);
            }

        int sum = 0;
        for(char ch1 : s1) sum += ch1;
        for(char ch2 : s2) sum += ch2;
        return sum - dp[m][n] - dp[m][n]; // 两个字符串 需要减两次
    }
};

8.最长重复子数组

在这里插入图片描述

思路

  1. 状态定义

    • dp[i][j] 表示 nums1 中前 i 个元素和 nums2 中前 j 个元素的最长公共子数组的长度。
  2. 初始化

    • dp 数组初始化为 0。默认情况下,如果 ij0,公共子数组的长度为 0
  3. 状态转移

    • 如果 nums1[i - 1]nums2[j - 1] 相等,则 dp[i][j] = dp[i-1][j-1] + 1。如果不相等,dp[i][j] 保持为 0
    • 更新 retdp[i][j] 和当前 ret 的最大值,得到最大长度的公共子数组。
  4. 返回值

    • 返回 ret,即最长公共子数组的长度。

时间复杂度O(m * n),需要遍历所有 m * n 的位置。

空间复杂度O(m * n),需要一个大小为 m * n 的 DP 表。

代码

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        // 创建dp数组 + 初始化
        // nums1以i为结尾的子数组 与 num2以j为结尾的子数组 最长重复子数组
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        // 填表
        int ret = 0;
        for(int i = 1; i <= m; ++i)
            for(int j = 1; j <= n; ++j)
            {
                if(nums1[i - 1] == nums2[j - 1])
                    dp[i][j] = dp[i-1][j-1] + 1, ret = max(ret, dp[i][j]);
            }
        
        return ret;
    }
};

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

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

相关文章

「OC」CAlayer——巧用动画实现一个丝滑的折叠cell

「OC」CAlayer——巧用动画实现一个丝滑的折叠cell 前言 在这个暑假集训后的时间&#xff0c;都在家里做着学习笔记的整理&#xff0c;深入学习了CALayer的相关知识&#xff0c;掌握了第三方库Masonry自动布局的用法&#xff0c;以及学习了MVC的相关内容&#xff0c;正好组内…

在Postgresql中计算工单的对应的GPS轨迹距离

一、概述 在某个App开发中&#xff0c;要求记录用户的日常轨迹&#xff0c;在用户巡逻设备的时&#xff0c;将记录的轨迹点当做该设备巡逻时候的轨迹。 由于业务逻辑上没有明确的指示人员巡逻工单-GPS位置之间的关系&#xff0c;所以通过时间关系进行轨迹划定。 二、创建测试表…

Core ML

本文翻译整理自&#xff1a;Core ML : https://developer.apple.com/cn/documentation/coreml/ 文章目录 一、概览二、获取 Core ML 模型三、将 Core ML 模型集成到你的 App 中1、将模型添加到您的Xcode项目2、在代码中创建模型3、获取要传递给模型的输入值4、使用模型进行预测…

vue2踩坑记录:el-select如何绑定对象

页面上的下拉框&#xff1a; 选中人员之后&#xff0c;需要使用人员的其它信息做后续操作。所以不能只绑定用户Id&#xff0c;需要绑定整个item(用户对象)&#xff0c;这样每次change事件所获取到的newValue是整个对象&#xff0c;而且v-model中的变量指向的也是整个对象&#…

Windows bat脚本学习五(函数)

一、简介 使用bat脚本时&#xff0c;经常要使用函数&#xff0c;而函数的传参和返回值也是非常重要的&#xff0c;今天就总结一下bat中函数相关的使用方法。 二、函数 1.函数调用 :函数名 在Bat中&#xff0c;使用“:函数名”来定义一个函数。 见如下代码&#xff1a; echo o…

安装KataGo+Sabaki围棋引擎

1.由于我的用户名是中文名,修改TMP和TEMP为SystemRoot(后续修改回来) 2.加入xx.bin文件后,cmd输入katago.exe genconfig -model model.bin -output gtp_custom.cfg 3.KataGo下载完成 4.配置GUI界面

ubuntu录屏解决ubuntu下无法播放MP4格式文件的方法

参考 gnome gnome是系统自带的录屏&#xff0c;通过ctrlshiftaltr触发 保存到了视频目录下&#xff0c;webm格式文件。 screencastify 这是一个chrome扩展&#xff0c;&#xff0c;一般不推荐使用 recapp 比gnome自由一些&#xff0c;可以自由屏幕录制。但是无法修改录制…

利用流水线实现版本一键发布

目录 1. 背景2. 实现步骤3.1 前置条件3.2 更新版本号和拉出发布分支3.3 生成 diffCommit3.4 自动触发联动编译3.5 让通知更友好 3. 总结 1. 背景 通常我们发布版本时会有这样几个步骤&#xff0c;更改版本号&#xff0c;拉出 release 分支&#xff0c;生成发布包。但是博主所在…

关于ThinkPHP 5 框架开启自动搜索控制器 无法访问的问题坑

假如当前有一个登陆接口功能 因为后续会有不同版本的 登陆接口 这时候 我们可以在控制器中 新建文件夹 做区分 方便管理即 新建了一个 api 模块 文件路径是 api/controller/V1/Login 正常情况下 controller 目录下 是 控制器文件 login.php 文件&#xff0c;由于我们有多个…

地下车库电车这样充电,必须改

文 | AUTO芯球 作者 | 响铃 我是真的害怕啊 你们看&#xff0c;我住的小区&#xff0c; 都是这样子啊 一个这样子的塑料盒&#xff0c;最多一个铝板盒子&#xff0c;就给车充电了 你看看 这样的塑料电槽 就这样裸露在外面了 有些摸一下 电线都在发烫 现在我们小区内这…

【NCom】:通用负压退火方法构建超高负载单原子催化剂库

摘要&#xff1a;由高密度单原子填充的催化剂系统对于提高催化活性和选择性至关重要&#xff0c;这可以最大限度地发挥异质单原子催化剂 (SAC) 的工业前景。然而&#xff0c;实现金属含量超过 10 wt% 的高负载 SAC 仍然具有挑战性。在这里&#xff0c;我们描述了一种通用的负压…

【UCB CS61C】Lecture 4 - C Memory Management Usage

目录 C 的内存布局&#xff08;Memory Layout&#xff09;栈&#xff08;Stack&#xff09;静态数据&#xff08;Static Data&#xff09;代码&#xff08;Code&#xff09; 寻址&#xff08;Addressing&#xff09;地址&#xff08;Address&#xff09;字节序&#xff08;Endi…

电脑缺少dll文件怎么解决?Dll文件修复工具使用教程(方法合集)

众所周知&#xff0c;dll文件是计算器中的一类文件。占据了Windows操作系统的重要地位&#xff0c;主要作用就是可以让多个程序在运行时加以使用。dll文件包含了数字、文本、界面的等内容。 电脑缺少dll文件怎么解决&#xff1f;如果你启动某个程序时&#xff0c;发现电脑提示缺…

如何共享EC2 AMI给其他AWS账户

在本篇文章中&#xff0c;我们将详细介绍如何通过Amazon Web Services (AWS) 的Elastic Compute Cloud (EC2) 平台&#xff0c;将自定义AMI&#xff08;Amazon Machine Image&#xff09;共享给其他AWS账户。接下来&#xff0c;我们九河云将一步步引导您完成整个过程&#xff0…

数据驱动,智领办公!陀螺匠·企业助手 v1.7公测版发布

在数字化转型浪潮中&#xff0c;企业对办公自动化系统的需求愈加强烈&#xff0c;追求高效、灵活、智能的办公管理解决方案成为行业共识&#xff0c;我们深知&#xff0c;只有不断创新和完善&#xff0c;才能满足企业日益增长的需求。此次&#xff0c;我们带来陀螺匠企业助手 v…

【GeoScenePro】知识图谱

视频教程: ArcGIS/GeoScene知识图谱入门篇_哔哩哔哩_bilibili 所需软件: GeoScene Pro桌面端产品 Geoscene Enterprise四大组件(GeoScene_Server、GeoScene_DataStore、GeoScene_Portal、GeoScene_Web_Adaptor) 安装 【GeoScenePortal】安装和部署-CSDN博客

2024年下半年软考备考前的注意细节点

一、备考教材信息——选择官方正版&#xff08;电子、纸质都可以&#xff09; 中项 书名&#xff1a;《系统集成项目管理工程师教程》&#xff08;第三版&#xff09; 出版社&#xff1a;清华大学出版社 书籍类型&#xff1a;全国计算机技术与软件专业技术资格&#xff08;…

centos安装docker并配置加速器

docker安装与卸载&#xff1a; 1、检查当前是否安装docker yum list installed | grep docker2、卸载docker 根据yum list installed | grep docker查询出来的内容&#xff0c;逐个进行删除 yum remove docker.x86 64 -y3、启动与关闭docker 4、删除/etc/docker文件夹 如果…

少走弯路,ESP32 读取Micro SD(TF)播放mp3的坑路历程。

这个坑采的非常冤枉和巨大&#xff0c;非常大的冤枉路&#xff0c;只能一声叹息 说一下我是如何踩坑的&#xff0c;原本是打算用esp32 读取SD卡播放mp3,在esp32 读取自己打的SD卡已经踩了无数坑了&#xff0c;详情见&#xff1a; 少走弯路&#xff0c;ESP32 使用Micro SD(TF)…

Java学习第五天

数组 数组适合做一批同类型数据的存储。 静态初始化数组&#xff1a; 注意&#xff1a;数组变量名中存储的是数组在内存中的地址&#xff0c;数组是引用类型。 数组的访问 动态初始化数组&#xff1a; 数组的遍历&#xff1a; 注意左边和右边的区别&#xff0c;一个是改变数组…