【动态规划】 深入动态规划—两个数组的dp问题

news2025/4/17 1:09:07

文章目录

  • 前言
  • 例题
    • 一、最长公共子序列
    • 二、不相交的线
    • 三、不同的子序列
    • 四、通配符匹配
    • 五、交错字符串
    • 六、两个字符串的最小ASCII删除和
    • 七、最长重复子数组
  • 结语


在这里插入图片描述

前言

问题本质
它主要围绕着给定的两个数组展开,旨在通过对这两个数组元素间关系的分析,找出满足特定目标的最优解。比如在字符串处理中,两个字符串可看作字符数组,求它们的最长公共子序列等问题;或是在数值数组场景下,计算从两个数组元素组合中得到的最大收益等。
解题关键要素
状态定义:通常构建一个二维数组 dp[i][j] 作为状态表示。这里的 i 对应第一个数组的下标,j 对应第二个数组的下标 。dp[i][j] 代表在考虑第一个数组前 i 个元素和第二个数组前 j 个元素时,问题的最优解值,像上述最长公共子序列问题中,dp[i][j] 就表示对应前缀子串的最长公共子序列长度。
状态转移方程:这是解决问题的核心。需深入剖析问题特性,明确 dp[i][j] 如何由已求解的子问题状态(如 dp[i - 1][j]、dp[i][j - 1]、dp[i - 1][j - 1] 等)推导得出。例如在求两个数组元素匹配的最大得分问题中,若当前两个数组元素匹配,dp[i][j] 可能由 dp[i - 1][j - 1] 加上对应得分转移而来;若不匹配,则需比较 dp[i - 1][j] 和 dp[i][j - 1] 取较大值。
边界条件:初始化 dp 数组的第一行和第一列。如在最长公共子序列问题里,dp[0][j] 和 dp[i][0] 都初始化为 0 ,因为一个空串与另一个串不存在公共子序列。
通过合理定义状态、精准推导状态转移方程并正确处理边界条件,就能借助动态规划高效解决涉及两个数组的各类问题。

下面本篇文章,将通过例题为大家详细动态规划中的两个数组的dp问题!

例题

一、最长公共子序列

  1. 题目链接:最长公共子序列
  2. 题目描述:

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共⼦序 列 ,返回 0 。 ⼀个字符串的 ⼦序列 是指这样⼀个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下 删除某些字符(也可以不删除任何字符)后组成的新字符串。 ◦ 例如,“ace” 是 “abcde” 的⼦序列,但 “aec” 不是 “abcde” 的⼦序列。
两个字符串的 公共⼦序列 是这两个字符串所共同拥有的⼦序列。 ⽰例 1: 输⼊:text1 = “abcde”, text2 = “ace”
输出:3 解释:最⻓公共⼦序列是 “ace” ,它的⻓度为 3 。 ⽰例 2: 输⼊:text1 = “abc”, text2 = “abc” 输出:3 解释:最⻓公共⼦序列是 “abc” ,它的⻓度为 3 。 ⽰例 3: 输⼊:text1 = “abc”, text2 = “def” 输出:0 解释:两个字符串没有公共⼦序列,返回 0 。 提⽰:1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由⼩写英⽂字符组成。

  1. 解法(动态规划):
    算法思路:
    状态表示: 对于两个数组的动态规划,我们的定义状态表⽰的经验就是:
    i. 选取第⼀个数组 [0, i] 区间以及第⼆个数组 [0, j] 区间作为研究对象;
    ii. 结合题目要求,定义状态表⽰。 在这道题中,我们根据定义状态表示为:
    dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,最长公共子序列的长度。
    状态转移方程: 分析状态转移方程的经验就是根据「最后⼀个位置」的状况,分情况讨论。 对于 dp[i][j] ,我们可以根据 s1[i] 与 s2[j] 的字符分情况讨论:
    i. 两个字符相同, s1[i] = s2[j] :那么最长公共子序列就在 s1 的 [0, i - 1] 以 及 s2 的 [0, j - 1] 区间上找到⼀个最长的,然后再加上 s1[i] 即可。因此dp[i][j] = dp[i - 1][j - 1] + 1 ;
    ii. 两个字符不相同, s1[i] != s2[j] :那么最长公共子序列⼀定不会同时以 s1[i]
    和 s2[j] 结尾。那么我们找最⻓公共⼦序列时,有下面三种策略:
    • 去 s1 的 [0, i - 1] 以及 s2 的 [0, j] 区间内找:此时最⼤⻓度为 dp[i - 1][j] ; • 去 s1 的 [0, i] 以及 s2 的 [0, j - 1] 区间内找:此时最大长度为 dp[i][j - 1] ;
    • 去 s1 的 [0, i - 1] 以及 s2 的 [0, j - 1] 区间内找:此时最大长度为dp[i - 1][j - 1] 。 我们要三者的最大值即可。但是我们细细观察会发现,第三种包含在第⼀种和第二种情况里面,但是我们求的是最大值,并不影响最终结果。因此只需求前两种情况下的最大值即可。 综上,状态转移方程为:
    if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1 ;
    if(s1[i] != s2[j]) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) 。
    初始化:
    a. 「空串」是有研究意义的,因此我们将原始 dp 表的规模多加上⼀⾏和⼀列,表⽰空串。
    b. 引入空串后,大大的方便我们的初始化。
    c. 但也要注意「下标的映射关系」,以及⾥⾯的值要「保证后续填表是正确的」。
    当 s1 为空时,没有长度,同理 s2 也是。因此第⼀⾏和第⼀列里面的值初始化为 0 即可保证后续填表是正确的。
    填表顺序: 根据「状态转移方程」得:从上往下填写每⼀行,每⼀行从左往右。
    返回值: 根据「状态表示」得:返回 dp[m][n]

  2. 代码示例:

 public int longestCommonSubsequence(String s1, String s2) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = s1.length(), n = s2.length();
        s1 = " " + s1;
        s2 = " " + s2;
        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) == s2.charAt(j))
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        return dp[m][n];
    }

二、不相交的线

  1. 题目链接:不相交的线
  2. 题目描述:

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制⼀些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:
• nums1[i] == nums2[j]
• 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于⼀条连线。 以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 1:
在这里插入图片描述
输⼊:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2 解释:可以画出两条不交叉的线,如上图所示。 但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从nums1[2]=2 到 nums2[1]=2 的直线相交。
示例 2: 输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3
示例 3: 输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

  1. 解法(动态规划):
    算法思路: 如果要保证两条直线不相交,那么我们「下⼀个连线」必须在「上⼀个连线」对应的两个元素的 「后面」寻找相同的元素。这不就转化成「最长公共子序列」的模型了嘛。那就是在这两个数组中 寻找「最长的公共子序列」。
    只不过是在整数数组中做⼀次「最长的公共子序列」,代码几乎⼀模⼀样,这里就不再赘述算法原理啦~

  2. 代码示例:

 public int maxUncrossedLines(int[] nums1, int[] nums2) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        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] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        return dp[m][n];
    }

三、不同的子序列

  1. 题目链接:不同的子序列
  2. 题目描述:

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 字符串的⼀个子序列是指,通过删除⼀些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的⼀个子序列,而 “AEC” 不是) 题目数据保证答案符合 32 位带符号整数范围。
示例 1:输入:s = “rabbbit”, t = “rabbit” 输出:3 解释:如下图所示, 有 3 种可以从 s 中得到 “rabbit” 的⽅案。
rabbbit
rabbbit
rabbbit
示例 2:输⼊:s = “babgbag”, t = “bag” 输出:5
解释:如下图所示, 有 5 种可以从 s 中得到 “bag” 的⽅案。
babgbag
babgbag
babgbag
babgbag
babgbag

  1. 解法(动态规划): 算法思路:
    状态表示: 对于两个字符串之间的 dp 问题,我们⼀般的思考方式如下:
    i. 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结 合题目的要求来定义「状态表示」;
    ii. 然后根据两个区间上「最后⼀个位置的字符」,来进行「分类讨论」,从而确定「状态转移方程」。
    我们可以根据上面的策略,解决大部分关于两个字符串之间的 dp 问题。
    dp[i][j] 表示:在字符串 s 的 [0, j] 区间内的所有子序列中,有多少个 t 字符串 [0,
    i] 区间内的子串。
    状态转移方程: 老规矩,根据「最后⼀个位置」的元素,结合题目要求,分情况讨论:
    i. 当 t[i] == s[j] 的时候,此时的⼦序列有两种选择: • ⼀种选择是:子序列选择 s[j] 作为结尾,此时相当于在状态 dp[i - 1][j - 1]
    中的所有符合要求的⼦序列的后⾯,再加上⼀个字符 s[j] (请大家结合状态表示,好好理解这句话),此时 dp[i][j] = dp[i - 1][j - 1] ; • 另⼀种选择是:我就是任性,我就不选择 s[j] 作为结尾。此时相当于选择了状态dp[i][j - 1] 中所有符合要求的⼦序列。我们也可以理解为继承了上个状态⾥⾯的 求得的⼦序列。此时 dp[i][j] = dp[i][j - 1] ; 两种情况加起来,就是 t[i] == s[j] 时的结果。
    ii. 当 t[i] != s[j] 的时候,此时的⼦序列只能从 dp[i][j - 1] 中选择所有符合要求的子序列。只能继承上个状态里面求得的子序列, dp[i][j] = dp[i][j - 1] ; 综上所述,状态转移方程为:
    ▪ 所有情况下都可以继承上⼀次的结果: dp[i][j] = dp[i][j - 1] ; ▪ 当 t[i] == s[j] 时,可以多选择⼀种情况: dp[i][j] += dp[i - 1][j - 1]
    初始化:
    a. 「空串」是有研究意义的,因此我们将原始 dp 表的规模多加上⼀⾏和⼀列,表⽰空串。
    b. 引⼊空串后,⼤⼤的⽅便我们的初始化。
    c. 但也要注意「下标的映射关系」,以及⾥⾯的值要「保证后续填表是正确的」。
    当 s 为空时, t 的子串中有⼀个空串和它⼀样,因此初始化第⼀⾏全部为 1 。
    填表顺序: 「从上往下」填每⼀行,每⼀行「从左往右」。
    返回值:根据「状态表⽰」,返回 dp[m][n] 的值。

  2. 代码示例:

 public int numDistinct(String s, String t) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = t.length(), n = s.length();
        int[][] dp = new int[m + 1][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.charAt(i - 1) == s.charAt(j - 1))
                    dp[i][j] += dp[i - 1][j - 1];
            }

        return dp[m][n];
    }

四、通配符匹配

  1. 题目链接:通配符匹配
  2. 题目描述:

给定⼀个字符串 (s) 和⼀个字符模式 § ,实现⼀个⽀持 ‘?’ 和 ’ * ’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
’ * ’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。 说明: s 可能为空,且只包含从 a-z 的小写字母。 p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和
示例 1:
输⼊: s = “aa” p = “a” 输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入: s = “aa” p = "
"
输出: true
解释: ‘’ 可以匹配任意字符串。
示例 3:
输入: s = “cb” p = “?a” 输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第⼆个 ‘a’ ⽆法匹配 ‘b’。 ⽰例 4:
输入: s = “adceb” p = “ab” 输出: true
解释: 第⼀个 '
’ 可以匹配空字符串, 第⼆个 '’ 可以匹配字符串 “dce”. ⽰例 5:
输⼊: s = “acdcb” p = "a
c?b" 输出: false

  1. 解法(动态规划):
    算法思路:
    状态表示: 对于两个字符串之间的 dp 问题,我们⼀般的思考⽅式如下:
    i. 选取第⼀个字符串的 [0, i] 区间以及第二个字符串的 [0, j] 区间当成研究对象,结合题目的要求来定义「状态表示」;
    ii. 然后根据两个区间上「最后⼀个位置的字符」,来进行「分类讨论」,从而确定「状态转移方程」。
    我们可以根据上⾯的策略,解决大部分关于两个字符串之间的 dp 问题。 因此,我们定义状态表示为:
    dp[i][j] 表示: p 字符串 [0, j] 区间内的子串能否匹配字符串 s 的 [0, i] 区间内的 ⼦串。
    状态转移方程: 老规矩,根据最后⼀个位置的元素,结合题目要求,分情况讨论:
    i. 当 s[i] == p[j] 或 p[j] == ‘?’ 的时候,此时两个字符串匹配上了当前的⼀个字 符,只能从 dp[i - 1][j - 1] 中看当前字符前⾯的两个子串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i][j - 1] ;
    ii. 当 p[j] == ‘’ 的时候,此时匹配策略有两种选择:
    • ⼀种选择是: * 匹配空字符串,此时相当于它匹配了⼀个寂寞,直接继承状态 dp[i][j - 1] ,此时 dp[i][j] = dp[i][j - 1] ;
    • 另⼀种选择是: * 向前匹配 1 ~ n 个字符,直⾄匹配上整个 s1 串。此时相当于 从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的 情况。此时 dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;
    iii. 当 p[j] 不是特殊字符,且不与 s[i] 相等时,无法匹配。 三种情况加起来,就是所有可能的匹配结果。 综上所述,状态转移⽅程为: ▪ 当 s[i] == p[j] 或 p[j] == ‘?’ 时: dp[i][j] = dp[i][j - 1] ; ▪ 当 p[j] == '
    ’ 时,有多种情况需要讨论: dp[i][j] = dp[k][j - 1] (0 <= k <= i) ; 优化:当我们发现,计算⼀个状态的时候,需要⼀个循环才能搞定的时候,我们要想到去优化。优 化的⽅向就是用⼀个或者两个状态来表示这⼀堆的状态。通常就是把它写下来,然后⽤数学的方式 做⼀下等价替换: 当 p[j] == '’ 时,状态转移方程为:dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1] …
    我们发现 i 是有规律的减小的,因此我们去看看 dp[i - 1][j] :dp[i - 1][j] = dp[i - 1][j - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1] …
    我们惊奇的发现, dp[i][j] 的状态转移⽅程⾥⾯除了第⼀项以外,其余的都可以用 dp[i - 1][j] 替代。因此,我们优化我们的状态转移⽅程为: dp[i][j] = dp[i - 1][j] || dp[i][j - 1] 。
    初始化: 由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为
    false 。 由于需要用到前一行和前一列的状态,我们初始化第一行、第⼀列即可。
    ◦ dp[0][0] 表⽰两个空串能否匹配,答案是显然的, 初始化为 true 。
    ◦ 第一行表示 s 是⼀个空串, p 串和空串只有⼀种匹配可能,即 p 串表示为 "
    *" ,此时 也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "" 的 p ⼦串和空串 的 dp 值设为 true 。 ◦ 第⼀列表示p 是⼀个空串,不可能匹配上 s 串,跟随数组初始化即可。
    填表顺序:
    从上往下填每一行,每⼀行从左往右。
    返回值: 根据状态表示,返回 dp[m][n] 的值。

  2. 代码示例

 public boolean isMatch(String ss, String pp) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回结果
        int m = ss.length(), n = pp.length();
        ss = " " + ss;
        pp = " " + pp;
        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();
        boolean[][] dp = new boolean[m + 1][n + 1];
        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];
    }

五、交错字符串

  1. 题目链接:
  2. 题目描述:

给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干非空子字符串: s = s1 + s2 + … + sn
t = t1 + t2 + … + tm |n - m| <= 1 交错 是 s1 + t1 + s2 + t2 + s3 + t3 + … 或者 t1 + s1 + t2 + s2 + t3 + s3 + …
注意:a + b 意味着字符串 a 和 b 连接。
示例 1:
在这里插入图片描述
输⼊:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac” 输出:true
示例 2:输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出:false
示例 3:输入:s1 = “”, s2 = “”, s3 = “”
输出:true

  1. 解法(动态规划): 算法思路: 对于两个字符串之间的 dp 问题,我们⼀般的思考⽅式如下:
    i. 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结 合题⽬的要求来定义「状态表⽰」;
    ii. 然后根据两个区间上「最后⼀个位置的字符」,来进⾏「分类讨论」,从⽽确定「状态转移 ⽅程」。
    我们可以根据上⾯的策略,解决⼤部分关于两个字符串之间的 dp 问题。
    这道题⾥⾯空串是有研究意义的,因此我们先预处理⼀下原始字符串,前⾯统⼀加上⼀个占位符:
    s1 = " " + s1, s2 = " " + s2, s3 = " " + s3 。
    状态表⽰:
    dp[i][j] 表⽰字符串 s1 中 [1, i] 区间内的字符串以及 s2 中 [1, j] 区间内的字符 串,能否拼接成 s3 中 [1, i + j] 区间内的字符串。
    状态转移⽅程: 先分析⼀下题目,题目中交错后的字符串为 s1 + t1 + s2 + t2 + s3 + t3… ,看 似⼀个 s ⼀个 t 。实际上 s1 能够拆分成更小的⼀个字符,进而可以细化成 s1 + s2 + s3 + t1 + t2 + s4… 。 也就是说,并不是前⼀个用了 s 的子串,后⼀个必须要用 t 的子串。这⼀点理解,对我们的状态转移很重要。
    继续根据两个区间上「最后⼀个位置的字符」,结合题目的要求,来进行「分类讨论」:
    i. 当 s3[i + j] = s1[i] 的时候,说明交错后的字符串的最后⼀个字符和 s1 的最后⼀ 个字符匹配了。那么整个字符串能否交错组成,变成:s1 中 [1, i - 1] 区间上的字符串以及 s2 中 [1, j] 区间上的字符串,能够交 错形成 s3 中 [1, i + j - 1] 区间上的字符串,也就是 dp[i - 1][j];此时 dp[i][j] = dp[i - 1][j]
    ii. 当 s3[i + j] = s2[j] 的时候,说明交错后的字符串的最后⼀个字符和 s2 的最后 ⼀个字符匹配了。那么整个字符串能否交错组成,变成:s1 中 [1, i] 区间上的字符串以及 s2 中 [1, j - 1] 区间上的字符串,能够交 错形成 s3 中 [1, i + j - 1] 区间上的字符串,也就是 dp[i][j - 1] ;
    iii. 当两者的末尾都不等于 s3 最后⼀个位置的字符时,说明不可能是两者的交错字符串。 上述三种情况下,只要有⼀个情况下能够交错组成目标串,就可以返回 true 。因此,我们可以定义状态转移为:
    dp[i][j] = (s1[i - 1] == s3[i + j - 1] && dp[i - 1][j]) || (s2[j - 1] == s3[i + j - 1] && dp[i][j - 1])
    只要有⼀个成立,结果就是 true 。
    初始化: 由于用到 i - 1 , j - 1 位置的值,因此需要初始化「第⼀个位置」以及「第⼀行」和「第⼀ 列」。
    ◦ 第⼀个位置:
    dp[0][0] = true ,因为空串 + 空串能够构成⼀个空串。
    ◦ 第⼀行: 第⼀行表示 s1 是⼀个空串,我们只用考虑 s2 即可。因此状态转移之和 s2 有关:
    dp[0][j] = s2[j - 1] == s3[j - 1] && dp[0][j - 1] , j 从 1 到 n ( n 为 s2 的⻓度) ◦ 第⼀列: 第⼀列表⽰ s2 是⼀个空串,我们只⽤考虑 s1 即可。因此状态转移之和 s1 有关:
    dp[i][0] = s1[i - 1] == s3[i - 1] && dp[i - 1][0] , i 从 1 到 m ( m 为 s1 的长度)
    填表顺序: 根据「状态转移」,我们需要「从上往下」填每⼀行,每一行「从左往右」。
    返回值: 根据「状态表示」,我们需要返回 dp[m][n] 的值。

  2. 代码示例:

public boolean isInterleave(String s1, String s2, String s3) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = s1.length(), n = s2.length();
        if (m + n != s3.length()) return false; // 处理下边界条件
        s1 = " " + s1;
        s2 = " " + s2;
        s3 = " " + s3;
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true; // 初始化
        for (int j = 1; j <= n; j++) // 初始化第⼀⾏
            if (s2.charAt(j) == s3.charAt(j)) dp[0][j] = true;
            else break;
        for (int i = 1; i <= m; i++) // 初始化第⼀列
            if (s1.charAt(i) == s3.charAt(i)) dp[i][0] = true;
            else break;

        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++)
                dp[i][j] = (s1.charAt(i) == s3.charAt(i + j) && dp[i - 1][j])
                        || (s2.charAt(j) == s3.charAt(i + j) && dp[i][j - 1]);
        return dp[m][n];
    }

六、两个字符串的最小ASCII删除和

  1. 题目链接:两个字符串的最小ASCII删除和
  2. 题目描述:

给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
示例 1:输⼊: s1 = “sea”, s2 = “eat” 输出: 231
解释:在 “sea” 中删除 “s” 并将 “s” 的值(115)加⼊总和。 在 “eat” 中删除 “t” 并将 116 加⼊总和。 结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最⼩和。 ⽰例 2:
输⼊: s1 = “delete”, s2 = “leet” 输出: 403
解释:
在 “delete” 中删除 “dee” 字符串变成 “let”, 将 100[d]+101[e]+101[e] 加⼊总和。在 “leet” 中删除 “e” 将 101[e] 加⼊总和。 结束时,两个字符串都等于 “let”,结果即为 100+101+101+101 = 403 。 如果改为将两个字符串转换为 “lee” 或 “eet”,我们会得到 433 或 417 的结果,比答案更大。

  1. 解法(动态规划):
    算法思路: 正难则反:求两个字符串的最小ASCII 删除和,其实就是找到两个字符串中所有的公共子序列里面, ASCII 最大和。 因此,我们的思路就是按照「最长公共子序列」的分析方式来分析。
    状态表示:
    dp[i][j] 表示: s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有的子序列中,公 共⼦序列的 ASCII 最⼤和。
    状态转移⽅程: 对于 dp[i][j] 根据「最后⼀个位置」的元素,结合题目要求,分情况讨论:
    i. 当 s1[i] == s2[j] 时:应该先在 s1 的 [0, i - 1] 区间以及 s2 的 [0, j - 1] 区间内找⼀个公共⼦序列的最⼤和,然后在它们后面加上⼀个 s1[i] 字符即可。 此时 dp[i][j] = dp[i - 1][j - 1] + s1[i] ;
    ii. 当 s1[i] != s2[j] 时:公共⼦序列的最大和会有三种可能:
    • s1 的 [0, i - 1] 区间以及 s2 的 [0, j] 区间内:此时 dp[i][j] = dp[i - 1][j] ; • s1 的 [0, i] 区间以及 s2 的 [0, j - 1] 区间内:此时 dp[i][j] = dp[i][j - 1] ; • s1 的 [0, i - 1] 区间以及 s2 的 [0, j - 1] 区间内:此时 dp[i][j] = dp[i - 1][j - 1] 。 但是前两种情况⾥⾯包含了第三种情况,因此仅需考虑前两种情况下的最⼤值即可。 综上所述,状态转移方程为:
    ◦ 当 s1[i - 1] == s2[j - 1] 时, dp[i][j] = dp[i - 1][j - 1] + s1[i] ;
    ◦ 当 s1[i - 1] != s2[j - 1] 时, dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    初始化:
    a. 「空串」是有研究意义的,因此我们将原始 dp 表的规模多加上⼀⾏和⼀列,表⽰空串。
    b. 引⼊空串后,大大的「方便我们的初始化」。
    c. 但也要注意「下标的映射」关系,以及里面的值要保证「后续填表是正确的」。
    当 s1 为空时,没有长度,同理 s2 也是。因此第一行和第⼀列里面的值初始化为 0 即可保证后续填表是正确的。
    填表顺序: 「从上往下」填每一行,每⼀行「从左往右」。
    返回值: 根据「状态表示」,我们不能直接返回 dp 表里面的某个值:
    i. 先找到 dp[m][n] ,也是最大公共 ASCII 和;
    ii. 统计两个字符串的 ASCII 码和 s u m;
    iii. 返回 sum - 2 * dp[m][n]

  2. 代码示例:

 public int minimumDeleteSum(String s1, String s2) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        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++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                if (s1.charAt(i - 1) == s2.charAt(j - 1))
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + s1.charAt(i - 1));
            }

        int sum = 0; // 统计元素和
        for (char ch : s1.toCharArray()) sum += ch;
        for (char ch : s2.toCharArray()) sum += ch;
        return sum - dp[m][n] - dp[m][n];
    }

七、最长重复子数组

  1. 题目链接:最长重复子数组
  2. 题目描述:

给两个整数数组 nums1 和 nums2 ,返回两个数组中公共的 、长度最长的子数组的长度 。
示例 1: 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3 解释:长度最长的公共子数组是 [3,2,1] 。
示例 2: 输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5

  1. 解法(动态规划): 算法思路: ⼦数组是数组中「连续」的⼀段,我们习惯上「以某⼀个位置为结尾」来研究。由于是两个数组, 因此我们可以尝试:以第⼀个数组的 i 位置为结尾以及第⼆个数组的 j 位置为结尾来解决问题。
    状态表示:
    dp[i][j] 表示「以第⼀个数组的 i 位置为结尾」,以及「第⼆个数组的 j 位置为结尾公共的、长度最长的「子数组」的长度。
    状态转移⽅程: 对于 dp[i][j] ,当 nums1[i] == nums2[j] 的时候,才有意义,此时最长重复子数组的长度应该等于 1 加上除去最后⼀个位置时,以 i - 1, j - 1 为结尾的最长重复子数组的长度。因此,状态转移方程为: dp[i][j] = 1 + dp[i - 1][j - 1]
    初始化:为了处理「越界」的情况,我们可以添加⼀行和一列, dp 数组的下标从 1 开始,这样就无需初始化。 第⼀行表示第⼀个数组为空,此时没有重复子数组,因此里面的值设置成 0 即可; 第一列也是同理。
    填表顺序: 根据「状态转移」,我们需要「从上往下」填每⼀行,每⼀行「从左往右」。
    返回值: 根据「状态表示」,我们需要返回 dp 表里面的「最大值」。

  2. 代码示例:

 public int findLength(int[] nums1, int[] nums2) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        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 = Math.max(ret, dp[i][j]);
                }
        return ret;
    }

结语

本文到这里就结束了,主要通过几道两个数组dp问题算法题,介绍了这种类型动态规划的做题思路,带大家深入了解了动态规划中两个数组dp问题 算法这一类型。

以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!

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

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

相关文章

结合大语言模型整理叙述并生成思维导图的思路

楔子 我比较喜欢长篇大论。这在代理律师界被视为一种禁忌。 我高中一年级的时候因为入学成绩好&#xff08;所在县榜眼名次&#xff09;&#xff0c;直接被所在班的班主任任命为班长。我其实不喜欢这个岗位。因为老师一来就要提前注意到&#xff0c;要及时喊“起立”、英语课…

【力扣hot100题】(073)数组中的第K个最大元素

花了两天时间搞明白答案的快速排序和堆排序。 两种都写了一遍&#xff0c;感觉堆排序更简单很多。 两种都记录一下&#xff0c;包括具体方法和易错点。 快速排序 class Solution { public:vector<int> nums;int quicksort(int left,int right,int k){if(leftright) r…

mapbox基础,加载F4Map二维地图

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性二、🍀F4Map 简介2.1 ☘️技术特点2.2 ☘️核…

Android:Android Studio右侧Gradle没有assembleRelease等选项

旧版as是“Do not build Gradle task list during Gradle sync” 操作这个选项。 参考这篇文章&#xff1a;Android Studio Gradle中没有Task任务&#xff0c;没有Assemble任务&#xff0c;不能方便导出aar包_gradle 没有task-CSDN博客 在as2024版本中&#xff0c;打开Setting…

DRAM CRC:让DDR5内存数据更靠谱

DRAM(动态随机存取存储器)是电脑内存的核心部件,负责存储和传输数据。如果数据在传输中出错,后果可能很严重,比如程序崩溃或者数据损坏。为了解决这个问题,DDR5内存引入了一个新功能,叫DRAM CRC(循环冗余校验)。简单来说,它是用来检查读写数据有没有问题的工具。 下面…

心率测量-arduino+matlab

参考&#xff1a;【教程】教你玩转Stduino之手指心跳检测模块 - 知乎 (zhihu.com) 1 原理 心跳检测模块&#xff0c;由一个红外线发射LED和红外接收器构成。手指心跳监测模块能够测量脉搏&#xff0c;是这样工作的&#xff1a;当手指放在发射器与接收器之间&#xff0c;红外发射…

H3C的MSTP+VRRP高可靠性组网技术(MSTP单域)

以下内容纯为博主分享自己的想法和理解&#xff0c;如有错误轻喷 MSTP多生成树协议可以基于不同实例实现不同VLAN之间的负载分担 VRRP虚拟路由器冗余协议可以提高网关的可靠性防止单点故障的可能 在以前这两种协议通常一起搭配组网&#xff0c;来提高网络的可靠性和稳定性&a…

字符串替换 (模拟)神奇数 (数学)DNA序列 (固定长度的滑动窗口)

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;每日两三题 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 字符串替换 &#xff08;模拟&#xff09;神奇数 &#xff08;数学&#xff09;DNA序列 &#xff08;固定长度的滑动窗口&am…

Linux驱动-块设备驱动

Linux驱动-块设备驱动 一&#xff0c;块设备驱动简介二&#xff0c;无请求队列情况&#xff08;EMMC和SD卡等&#xff09;三&#xff0c;请求队列情况&#xff08;磁盘等带有I/O调度的设备&#xff09;四&#xff0c;两者在驱动上区别 一&#xff0c;块设备驱动简介 块设备驱动…

【算法学习】链表篇:链表的常用技巧和操作总结

算法学习&#xff1a; https://blog.csdn.net/2301_80220607/category_12922080.html?spm1001.2014.3001.5482 前言&#xff1a; 在各种数据结构中&#xff0c;链表是最常用的几个之一&#xff0c;熟练使用链表和链表相关的算法&#xff0c;可以让我们在处理很多问题上都更加…

2台8卡L20服务器集群推理方案

1、整体流程梳理 #mermaid-svg-0aNtsWUnOH7ewXpN {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-0aNtsWUnOH7ewXpN .error-icon{fill:#552222;}#mermaid-svg-0aNtsWUnOH7ewXpN .error-text{fill:#552222;stroke:#55…

HarmonyOS:使用geoLocationManager (位置服务)获取位置信息

一、简介 位置服务提供GNSS定位、网络定位&#xff08;蜂窝基站、WLAN、蓝牙定位技术&#xff09;、地理编码、逆地理编码、国家码和地理围栏等基本功能。 使用位置服务时请打开设备“位置”开关。如果“位置”开关关闭并且代码未设置捕获异常&#xff0c;可能导致应用异常。 …

系统分析师(二)--操作系统

概述 进程管理 选项A&#xff1a;该进程中打开的文件 进程中打开的文件是由整个进程来管理的&#xff0c;同一进程下的各个线程都可以对这些打开的文件进行访问和操作&#xff0c;所以进程中打开的文件是可以被这些线程共享的。 选项B&#xff1a;该进程的代码段 进程的代码…

安科瑞测频仪表:新能源调频困局的破局者

安科瑞顾强 在“双碳”目标推动下&#xff0c;风电、光伏等新能源正加速成为电力供应的核心力量。然而&#xff0c;新能源发电的间歇性与波动性&#xff0c;如同一把“双刃剑”&#xff0c;在提供清洁电力的同时&#xff0c;也给电网稳定运行带来了前所未有的挑战。国家能源局…

富士相机照片 RAF 格式如何快速批量转为 JPG 格式教程

富士&#xff08;Fujifilm&#xff09;相机拍摄的 RAW 格式文件&#xff08;RAF&#xff09;因其高质量和丰富的图像信息而受到摄影师的喜爱。然而&#xff0c;RAF 文件通常体积较大且不易于分享或直接使用。为了方便处理&#xff0c;许多人选择将其转换为更通用的 JPG 格式。在…

Linux 入门指令(1)

&#xff08;1&#xff09;ls指令 ls -l可以缩写成 ll 同时一个ls可以加多个后缀 比如 ll -at (2)pwd指令 &#xff08;3&#xff09;cd指令 cd .是当前目录 &#xff08;4&#xff09;touch指令 &#xff08;5&#xff09;mkdir指令 &#xff08;6&#xff09;rmdir和rm…

Redis缓存数据库一致性

前言&#xff1a; 在系统开发中经常使用关系型数据库&#xff0c;为了提升关系型数据库的读性能&#xff0c;一般会使用redis加一层缓存&#xff0c;缓存和数据库是分离的两次操作&#xff0c;本文用来分析如何操作能保证缓存和数据库的数据一致性。 一、读场景 二、写场景 …

Android Coil 3 Fetcher大批量Bitmap拼接成1张扁平宽图,Kotlin

Android Coil 3 Fetcher大批量Bitmap拼接成1张扁平宽图&#xff0c;Kotlin <uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE" /><u…

文件相关:treecpmv命令扩展详解

拷贝和移动文件 序号命令对应英文作用01tree [目录名]tree以树状图列出文件目录结构02cp 源文件 目标文件copy复制文件或者目录03mv 源文件 目标文件move移动文件或者目录&#xff0f;文件或者目录重命名 一、 tree命令 &#xff08;1&#xff09;定义 tree 命令可以以树状…

S32K144的m_data_2地址不够存,重新在LD文件中配置地址区域

在开发平台软件的时候代码中超出了64K的内存&#xff0c;单纯在ld文件中&#xff0c;增加m_data_2的存储长度&#xff0c;原先是0x00007000,我将长度修改为0x00008000,起始地址还是0x20000000,软件编译没有报错堆栈超出&#xff0c;但是软件下载到单片机中之后&#xff0c;144不…