Day39:动态规划part12(115.不同的子序列、583.两个字符串的删除操作、72.编辑距离)

115.不同的子序列

题目链接https://leetcode.cn/problems/distinct-subsequences/description/

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

提示:

  • 0 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成

总结

1. 定义 DP 数组

定义 dp[i][j] 为:s 的前 i 个字符组成的子串中,出现 t 的前 j 个字符组成的子串的个数。

  • s 的长度为 mt 的长度为 n

  • dp 的大小为 (m + 1) x (n + 1)

2. 初始化

  • 当 t 为空字符串时:

    无论 s 是什么,空字符串都是 s 的子序列(删除所有字符即可),且只有 1 种方式。

    所以,对于任意 i,dp[i][0] = 1。

  • 当 s 为空字符串但 t 不为空时:

    空字符串无法生成非空字符串。

    所以,对于任意 j > 0,dp[0][j] = 0。

3. 状态转移方程

我们需要遍历 s 的每个字符(索引 i 从 1 到 m)和 t 的每个字符(索引 j 从 1 到 n)。

注意:在代码中访问字符串字符时,要用 s.charAt(i-1) 和 t.charAt(j-1)。

有两种情况:

  • 情况一:当前字符不匹配 (s[i-1] != t[j-1])

    如果 s 的当前字符和 t 的当前字符不同,那么 s 的这个字符对组成 t 没有任何帮助。

    此时,在 s 的前 i 个字符中找到 t 的个数,就等于在 s 的前 i-1 个字符中找到 t 的个数。

    java 复制代码
    dp[i][j] = dp[i-1][j];
  • 情况二:当前字符匹配 (s[i-1] == t[j-1])

    如果两个字符相同,我们有两种选择:

    1. 使用 s[i-1] 进行匹配 :这意味着我们用掉了 s 的这个字符和 t 的这个字符。剩下的问题变成了:在 s 的前 i-1 个字符中找 t 的前 j-1 个字符。方案数为 dp[i-1][j-1]

    2. 不使用 s[i-1] 进行匹配 :虽然字符相同,但我们可以选择"跳过"s 的这个字符,继续尝试用 s 前面的部分来匹配整个 t(也许 s 前面还有同样的字符)。方案数为 dp[i-1][j]

    总方案数为两者之和:

    java 复制代码
    dp[i][j] = dp[i-1][j] + dp[i-1][j-1];

    4. 图解示例

    s = "babgbag", t = "bag" 为例,表格填充逻辑如下:

    "" (0) b (1) a (2) g (3)
    "" (0) 1 0 0 0
    b (1) 1 1 0 0
    a (2) 1 1 1 0
    b (3) 1 2 1 0
    g (4) 1 2 1 1
    b (5) 1 3 1 1
    a (6) 1 3 4 1
    g (7) 1 3 4 5
  • 解释 dp[3][1] (s="bab", t="b"):

    • s[2]是'b',t[0]是'b'。

    • 用s的第二个'b': 看 dp[2][0] (值为1)。

    • 不用s的第二个'b': 看 dp[2][1] (值为1)。

    • 总数 = 1 + 1 = 2。

5. 代码实现

java 复制代码
class Solution {
    public int numDistinct(String s, String t) {
        int m = s.length();
        int n = t.length();

        // 如果 s 比 t 短,肯定无法组成子序列
        if (m < n) {
            return 0;
        }

        // 定义 dp 数组,多一行一列处理空字符串的情况
        // dp[i][j] 表示 s 的前 i 个字符包含 t 的前 j 个字符的方案数
        int[][] dp = new int[m + 1][n + 1];

        // 初始化:当 t 为空字符串时,只有一种方案(即删除 s 的所有字符)
        for (int i = 0; i <= m; i++) {
            dp[i][0] = 1;
        }
        
        // 注意:Java int数组默认初始化为0,所以 dp[0][j] (j>0) 已经是0了,无需手动设置

        // 开始填充 dp 表
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 注意:字符串索引从 0 开始,所以是 charAt(i-1) 和 charAt(j-1)
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    // 字符匹配:
                    // 1. 用 s[i-1] 匹配 t[j-1] -> dp[i-1][j-1]
                    // 2. 不用 s[i-1] 匹配 (跳过它) -> dp[i-1][j]
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    // 字符不匹配:只能跳过 s[i-1],沿用之前的匹配结果
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[m][n];
    }
}

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

题目链接https://leetcode.cn/problems/delete-operation-for-two-strings/description/

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: "sea", "eat"
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

总结

1. 定义 DP 数组

dp[i][j] 表示:想要让 word1 的前 i 个字符和 word2 的前 j 个字符变得相同,所需的 最小步数(最小删除次数)

2. 初始化

  • 当 word2 为空 (j=0):

    word1 的前 i 个字符要变成空字符串,必须把这 i 个字符全删掉。

    所以 dp[i][0] = i。

  • 当 word1 为空 (i=0):

    同理,word2 的前 j 个字符要变成空字符串,必须全删掉。

    所以 dp[0][j] = j。

3. 状态转移方程

我们遍历 word1 (索引 i) 和 word2 (索引 j)。

  • 情况一:当前字符相同 (word1[i-1] == word2[j-1])

    既然两个字符一样,那就不需要删除它们,它们可以作为最终结果的一部分。

    这时候的最小步数等于去掉这两个字符之前的状态。

    java 复制代码
    dp[i][j] = dp[i-1][j-1]
  • 情况二:当前字符不同 (word1[i-1] != word2[j-1])

    既然不同,为了让它们相同,我们必须至少删除其中一个字符。我们选代价最小的那种方案:

    1. 删除 word1 的当前字符 :步数是 dp[i-1][j] + 1

    2. 删除 word2 的当前字符 :步数是 dp[i][j-1] + 1

    取两者的最小值:

    java 复制代码
    dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1

4. 图解示例

输入:word1 = "sea", word2 = "eat"

"" (0) e (1) a (2) t (3)
"" (0) 0 1 2 3
s (1) 1 2 3 4
e (2) 2 1 2 3
a (3) 3 2 1 2
  • dp[2][1] ('se' vs 'e'):

    • word1 是 'e', word2 是 'e'。相等!

    • 直接继承左上角 dp[1][0] (值为 1)。

    • 解释:'s' vs "" 需要删1步。加上相同的 'e' 后,步数不变。

  • dp[3][3] ('sea' vs 'eat'):

    • word1 是 'a', word2 是 't'。不相等。

    • 删 'a' (看上方 dp[2][3]=3) -> 3+1=4。

    • 删 't' (看左方 dp[3][2]=1) -> 1+1=2。

    • 取最小:2。

5. 代码实现

java 复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();

        // dp[i][j] 表示 word1的前i位 和 word2的前j位 达到相同所需的最小删除步数
        int[][] dp = new int[m + 1][n + 1];

        // 初始化第一列:word2为空,word1需要删 i 次
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i;
        }
        // 初始化第一行:word1为空,word2需要删 j 次
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j;
        }

        // 填充 DP 表
        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 {
                    // 如果字符不同,尝试删 word1 或 删 word2,取最小代价 + 1
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
                }
            }
        }

        return dp[m][n];
    }
}

72.编辑距离

题目链接https://leetcode.cn/problems/edit-distance/description/

给你两个单词 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')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1 和 word2 由小写英文字母组成

总结

1. 定义 DP 数组

dp[i][j] 表示:将 word1 的前 i 个字符转换成 word2 的前 j 个字符,所需要的 最小操作数

2. 初始化

  • 当 word2 为空 (j=0):

    要把 word1 的前 i 个字符变成空串,只能全部删除。

    java 复制代码
    dp[i][0] = i
  • 当 word1 为空 (i=0):

    要把空串变成 word2 的前 j 个字符,只能全部插入。

    java 复制代码
    dp[0][j] = j

3. 状态转移方程

我们比较 word1[i-1]word2[j-1]

情况一:字符相同 (word1[i-1] == word2[j-1])

既然字符一样,我们不需要做任何操作。当前的最小步数直接继承自不包含这两个字符的状态。

java 复制代码
dp[i][j] = dp[i-1][j-1]

情况二:字符不同 (word1[i-1] != word2[j-1])

我们必须进行一次操作(步数 +1)。我们有三种选择,取最小的那个:

  1. 插入 (Insert):

    我们在 word1 的末尾插入一个字符来匹配 word2[j-1]。

    这就相当于:我们搞定了 word2 的当前字符,但 word1 的当前字符还没动。

    java 复制代码
    dp[i][j-1] + 1
  2. 删除 (Delete):

    我们删除 word1 的当前字符。

    这就相当于:word1 的这个字符废了,我们要用 word1 前面的部分去匹配 word2。

    java 复制代码
    dp[i-1][j] + 1
  3. 替换 (Replace):

    我们将 word1 的当前字符直接替换成 word2 的当前字符。

    这就相当于:两个字符都处理完了,大家都退一步。

    java 复制代码
    dp[i-1][j-1] + 1

总结方程:

java 复制代码
dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1

4. 图解示例

horse -> ros

"" (0) r (1) o (2) s (3)
"" (0) 0 1 2 3
h (1) 1 1(替) 2 3
o (2) 2 2 1(等) 2
r (3) 3 2(等) 2 2
s (4) 4 3 3 2(等)
e (5) 5 4 4 3
  • dp[5][3] (horse vs ros):

    • e vs s 不等。

    • 左边 dp[5][2] (horse -> ro) = 4,插入 's' -> 5。

    • 上边 dp[4][3] (hors -> ros) = 2,删除 'e' -> 3。

    • 左上 dp[4][2] (hors -> ro) = 2,替换 'e' 为 's' -> 3。

    • 最小值是 3。

5. 代码实现

java 复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();

        // dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符的最少操作数
        int[][] dp = new int[m + 1][n + 1];

        // 初始化:word1 为空变成 word2,需要插入 j 次
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j;
        }
        // 初始化:word1 变成空 word2,需要删除 i 次
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i;
        }

        // 遍历填充
        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-1][j-1] + 1  => 替换
                    // dp[i][j-1] + 1    => 插入
                    // dp[i-1][j] + 1    => 删除
                    dp[i][j] = Math.min(dp[i - 1][j - 1], 
                               Math.min(dp[i][j - 1], dp[i - 1][j])) + 1;
                }
            }
        }

        return dp[m][n];
    }
}
相关推荐
历程里程碑5 小时前
C++ 10 模板进阶:参数特化与分离编译解析
c语言·开发语言·数据结构·c++·算法
星辞树5 小时前
从 In-context Learning 到 RLHF:大语言模型的范式跃迁
算法
再__努力1点6 小时前
【68】颜色直方图详解与Python实现
开发语言·图像处理·人工智能·python·算法·计算机视觉
mingchen_peng6 小时前
第一章 初识智能体
算法
百锦再6 小时前
国产数据库的平替亮点——关系型数据库架构适配
android·java·前端·数据库·sql·算法·数据库架构
晨曦夜月6 小时前
笔试强训day5
数据结构·算法
H_z___6 小时前
Hz的计数问题总结
数据结构·算法
她说彩礼65万6 小时前
C# 反射
java·算法·c#