动态规划---最长子串/子序列问题序列2

本文详细介绍了动态规划在求解最长回文子序列(516. 最长回文子序列)、最长公共子序列(1143. 最长公共子序列)和编辑距离(72. 编辑距离)问题中的应用。通过状态定义、状态转移方程、初始化和最终输出的解析,阐述了解决这些问题的关键步骤和逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划---最长子串/子序列问题序列2

516. 最长回文子序列

给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。

示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。

示例 2:
输入:
"cbbd"
输出:
2
一个可能的最长回文子序列为 "bb"。

解法:动态规划

  • 1、状态定义:在子串 s[i..j] 中,最长回文子序列的长度为 dp[i][j]。
  • 2、状态转移方程:如果我们想求dp[i][j],假设你知道了子问题 dp[i+1][j-1] 的结果(s[i+1…j-1] 中最长回文子序列的长度),你是否能想办法算出 dp[i][j] 的值(s[i…j] 中,最长回文子序列的长度)呢?
    在这里插入图片描述
    可以!这取决于 s[i] 和 s[j] 的字符:
    如果它俩相等,那么它俩加上 s[i+1…j-1] 中的最长回文子序列就是 s[i…j] 的最长回文子序列:
    在这里插入图片描述

如果它俩不相等,说明它俩不可能同时出现在 s[i…j] 的最长回文子序列中,那么把它俩分别加入 s[i+1…j-1] 中,看看哪个子串产生的回文子序列更长即可:
在这里插入图片描述
以上两种情况写成伪代码就是这样:

if (s[i] == s[j])
    // 它俩一定在最长回文子序列中
    dp[i][j] = dp[i + 1][j - 1] + 2;
else
    // s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

至此,状态转移方程就写出来了,可以看出:
想求 dp[i][j] 需要知道 dp[i+1][j-1],dp[i+1][j],dp[i][j-1] 这三个位置

  • 3、初始化(base case): 如果只有一个字符,显然最长回文子序列长度是 1,也就是 dp[i][j] = 1 (i == j)。
  • 4、最终输出:我们最终要求的就是 dp[0][n - 1],也就是整个 s 的最长回文子序列的长度。
代码:
class Solution {
    public int longestPalindromeSubseq(String s) {
        if (s.equals("") || s.length() == 0) {
            return 0;
        }
        int len = s.length();
        //状态定义:对 dp[i][j]在子串 s[i..j] 中,最长回文子序列的长度为 dp[i][j]
        int[][] dp = new int[len][len];
        for (int i=0; i<len; i++) {
			//一个字符时,最长回文子序列长度为1
            dp[i][i] = 1;
        }
        //这里的j的初始值为什么是i+1?
        //因为对 dp 数组的定义是:在子串 s[i..j] 中,最长回文子序列的长度为 dp[i][j],所以j永远比i大,也可以通过最终的值为dp[0][len-1],反向推遍历过程
        for (int i=len-1; i>=0; i--) {
            for (int j=i+1; j<len; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i+1][j-1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
                }
            }
        }
        return dp[0][len-1];
    }
}

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。

示例 1:
输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0

解法:动态规划

  • 1、状态定义:dp[i][j] 的含义是:对于 s1[1..i] 和 s2[1..j],它们的最长公共子序列 长度是 dp[i][j]。
    比如说对于字符串 s1 和 s2,一般来说都要构造一个这样的 DP table:
    在这里插入图片描述
    为了方便理解此表,我们暂时认为索引是从 1 开始的,待会的代码中只要稍作调整即可。其中,dp[i][j] 的含义是:对于 s1[1…i] 和 s2[1…j],它们的 LCS 长度是 dp[i][j]。
    比如上图的例子,d[2][4] 的含义就是:对于 “ac” 和 “babc”,它们的 LCS 长度是 2。我们最终想得到的答案应该是 dp[3][6]。
  • 2、状态转移方程:此题是求 s1 和 s2 的最长公共子序列,不妨称这个最终的子序列为 lcs。那么对于 s1 和 s2 中的每个字符,有什么选择?很简单,两种选择,要么在 lcs 中,要么不在。
    在这里插入图片描述
    以下分成两种情况进行解释:
    情况1: 如果 str1[i] == str2[j],那么这个字符肯定在最后的公共子序列中,那么这个字符一定会被被选择:
    此时状态转移方程:
dp(i, j) = dp(i - 1, j - 1) + 1。这个1就是应该被加入的这个字符

情况2: 如果 str1[i] != str2[j],那么在最终的公共子序列中只会存在 :str1[i] 和 str2[j] 这两个字符中的一个,或者一个都不存在。
此时的状态转移方程:

dp(i, j) =  Math.max(Math.max(dp(i-1, j), dp(i, j-1)) , dp[i-1][j-1])  

这里根据状态定义:dp[i][j] 的含义是:对于 s1[1…i] 和 s2[1…j],它们的最长公共子序列 长度是 dp[i][j]。可以得出:dp(i-1, j)和dp(i, j-1)两个状态的范围都大于且包括dp(i-1,j-1)的范围,所以取max的时候永远取不到dp[i-1][j-1],所以可以简化为:

   dp(i, j) =  Math.max(dp(i-1, j), dp(i, j-1))  

最终的状态转移方程:
在这里插入图片描述

  • 3、初始化(base case): 在i == 0 || j == 0时 , return 0
  • 4、最终输出:我们最终要求的就是 dp[len1][len2],也就是遍历完两个字符串后最长的公共子序列(理解下范围越大,公共子序列可能也越大)。
代码:
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();
		if (len1 == 0 || len2 == 0) {
            return 0;
        }

        //1 dp(i, j) 表示在text1的前i个字符, text2的前j个字符的能组成的公共子序列的长度, dp[i][j] 的含义是:对于 s1[1..i] 和 s2[1..j],它们的 LCS 长度是 dp[i][j]。
        int[][] dp = new int[len1+1][len2+1];
        //2 边界:i == 0 || j == 0   , return 0
        for (int i=0; i<=len2; i++) {
            dp[0][i] = 0;
        }
        for (int i=0; i<=len1; i++) {
            dp[i][0] = 0;
        }
        //3 由dp[i][j]的含义得出i和j起始值均为1,不是0
        for (int i=1; i<=len1; i++) {
            for (int j=1; j<=len2; j++) {
                //4 这里的i-1和j-1可以从数组起点0开始理解(用具体值代入估算)
                if (text1.charAt(i-1) == text2.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]);
                }
            }
        }
        return dp[len1][len2];
    }
}

注意: 该题有很多数组越界陷阱,详情看代码里的注释。

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse ('h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention ('i' 替换为 'e')
enention -> exention ('n' 替换为 'x')
exention -> exection ('n' 替换为 'c')
exection -> execution (插入 'u')

解法:动态规划
「动态规划」告诉我们可以「自底向上」去考虑一个问题,一般思路是:先想这个问题最开始是什么情况,比如这个问题是两个字符串都为空字符的时候,然后逐个地,一个字符一个字符加上去,在加字符的过程中考虑「状态转移」;
由于要考虑空字符,因此状态空间要多设置一行、多设置一列。具体按照以下步骤进行:

  • 1、 状态定义:设置一个二维的数组dp, dp[i][j] 表示将 word1[0, i) 转换成为 word2[0, j) 的最少操作数。

  • 2、 状态转移方程:
    情况 1:word1[i] == word2[j]
    如果 word1[i] == word2[j] 成立,则将 word1[0, i) 转换成为 word2[0, j) 的最少操作数等于 word1[0, i - 1) 转换成为 word2[0, j - 1) 的最少操作数,即: dp[i][j] = dp[i-1][j-1].
    情况 2:word1[i] != word2[j]
    如果 word1[i] != word2[j] ,则将 word1[0, i) 转换成为 word2[0, j) 的方案数就等于下面 3 种情况中的最少操作数(「最优子结构」):
    分别为“dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。”解释如下:
    (1) dp[i-1][j-1],表示word1的前(i-1)个字符已经转换成word2的前(j-1)个字符,两个单词都还剩最后一个字符未转换,这时候,直接将word1和word2的最后一个字符都替换成一样的字符即可完成全部替换。
    (2) dp[i][j-1],表示word1的前i个字符已经转换成word2的前(j-1)个字符,现在只有单词word2还剩最后一个字符未转换,这时候,直接在word1后插入word2最后一个还未转换的字符即可完成全部替换。
    (3) dp[i-1][j],表示word1的前i-1个字符已经转换成word2的前i个字符,现在只有单词word1还剩最后一个字符未转换,这时候因为word1的前i-1个字符已经完成了对word2的i个字符的转换,说明word1中最后一个未转换的字符是多余,直接将word1的最后一个字符给删除即可完成全部替换。
    简而言之:dp[i-1][j-1]到dp[i][j]需要进行替换操作,dp[i-1][j]到d[i][j]需要进行删除操作,dp[i][j-1] 到d[i][j]需要进行添加操作。
    这里的替换/删除/添加操作就是代码中的“+1”

    • (4) 最后在这 3 种操作中取最小值。
 dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1

映射成dp table如下图:
在这里插入图片描述

  • 3 初始化:从一个字符串变成空字符串,非空字符串的长度就是编辑距离;
    以下代码其实就是在填表格的第 0 行、第 0 列。
 		 for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
         }

         for (int j = 0; j <= len2; j++) {
            dp[0][j] = j;
         }
  • 4、 最终输出:dp[len1][len2] 符合语义,即 word1[0, len1) 转换成 word2[0, len2) 的最小操作数。(这里 ) 表示开区间。)
代码:
class Solution {
public int minDistance(String word1, String word2) {
		//1 边界
        if (word1.length() == 0 || word2.length() == 0) {
            return word2.length() + word1.length();
        }

        int m = word1.length();
        int n = word2.length();
        //2 状态定义:dp[i][j]:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离
        int[][] dp = new int[m+1][n+1];
        //3 base case
        for (int i=1; i<=m; i++) {
            dp[i][0] = i;
        }
        for (int i=1; i<=n; i++) {
            dp[0][i] = i;
        }
        //4 自底向上求解(状态转移)
        for (int i=1; i<=m; i++) {
            for (int j=1; j<=n; j++) {
                if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];   // 啥都不做
                } else {	 
				  //此步也算一步:   删除           插入          替换
                 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1;
                }
            }
        }
        // 存储整个Word1和word2的最小编辑距离
        return dp[m][n];
    }

    private int min(int a, int b, int c) {
        return Math.min(a, Math.min(b,c));
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值