力扣练习题(2024/4/19)

1两个字符串的删除操作

给定两个单词 word1word2 ,返回使得 word1word2相同 所需的最小步数

每步可以删除任意一个字符串中的一个字符。

示例 1:

复制代码
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

复制代码
输入:word1 = "leetcode", word2 = "etco"
输出:4

提示:

  • 1 <= word1.length, word2.length <= 500
  • word1word2 只包含小写英文字母

动态规划思路:

  1. 定义 dp[i][j] 为以第 i-1 个字符为结尾的字符串 word1,和以第 j-1 个字符为结尾的字符串 word2,想要达到相等,所需要删除元素的最少次数。

  2. 初始化 dp 数组:

    • word2 为空字符串时,word1 的任意子串都需要删除所有字符才能与之相等,因此 dp[i][0] = i
    • word1 为空字符串时,无论 word2 如何,都不需要删除任何字符,因此 dp[0][j] = 0。这一步在代码中被默认初始化为 0。
  3. 状态转移方程:

    • word1[i - 1] == word2[j - 1],即 word1 的第 i-1 个字符与 word2 的第 j-1 个字符相等时,不需要删除字符,dp[i][j] 可以由 dp[i - 1][j - 1] 继承而来。
    • word1[i - 1] != word2[j - 1],即 word1 的第 i-1 个字符与 word2 的第 j-1 个字符不相等时,此时需要删除字符,dp[i][j] 可以由以下几种操作得到:
      1. 删除 word1 的第 i-1 个字符,转化为以 word1 的前 i-2 个字符与 word2 的前 j-1 个字符相等的状态,操作数加一,即 dp[i][j] = dp[i - 1][j] + 1
      2. 删除 word2 的第 j-1 个字符,转化为以 word1 的前 i-1 个字符与 word2 的前 j-2 个字符相等的状态,操作数加一,即 dp[i][j] = dp[i][j - 1] + 1
      3. 同时删除 word1 的第 i-1 个字符和 word2 的第 j-1 个字符,转化为以 word1 的前 i-2 个字符与 word2 的前 j-2 个字符相等的状态,操作数加二,即 dp[i][j] = dp[i - 1][j - 1] + 2
  4. 最终返回 dp[word1.size()][word2.size()],即以 word1word2 为结尾的字符串相等所需要删除的最少次数。

代码:

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        // 创建二维数组 dp,dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
        
        // 初始化 dp 数组:当 word2 为空字符串时,需要删除 word1 的所有字符;当 word1 为空字符串时,不需要删除任何字符
        for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
        for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
        
        // 动态规划计算最小操作数
        for (int i = 1; i <= word1.size(); i++) {
            for (int j = 1; j <= word2.size(); j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    // 当前字符相等,不需要额外操作,继承前一个状态的操作数
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 当前字符不相等,选择删除 word1 的第 i 个字符或者删除 word2 的第 j 个字符中操作数较小的情况
                    dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
                }
            }
        }
        
        // 返回将 word1 转换为 word2 所需的最小操作数
        return dp[word1.size()][word2.size()];
    }
};

2编辑距离

给你两个单词 word1word2请返回将 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
  • word1word2 由小写英文字母组成

思路:

  1. 定义 `dp[i][j]` 表示以下标 i-1 为结尾的字符串 `word1`,和以下标 j-1 为结尾的字符串 `word2`,最近编辑距离为 `dp[i][j]`。

  2. 初始化二维数组 `dp`: - 初始化 `dp[i][0] = i`,表示当 `word2` 为空字符串时,将 `word1` 的前 `i` 个字符全部删除即可,操作次数为 `i`。 - 初始化 `dp[0][j] = j`,表示当 `word1` 为空字符串时,将 `word2` 的前 `j` 个字符全部插入即可,操作次数为 `j`。

  3. 动态规划求解最小编辑距离: - 对于每个位置 `(i, j)`,如果 `word1[i - 1]` 等于 `word2[j - 1]`,说明当前字符相同,不需要进行操作,取左上角的值 `dp[i - 1][j - 1]`; - 如果当前字符不同,可以选择插入、删除、替换操作中的最小值,并加上一次操作: - `dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1`。

  4. 最终返回 `dp[word1.size()][word2.size()]`,即将 `word1` 转换成 `word2` 所需的最小操作次数。 这种动态规划的方法利用填表法,逐步计算出最小编辑距离,从而实现字符串之间的匹配和转换。

代码:

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        // 创建二维数组 dp,dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最小操作次数
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
        
        // 初始化 dp 数组的第一行和第一列
        for(int i = 0; i <= word1.size(); i++) 
            dp[i][0] = i; // word2 为空字符串时,将 word1 的前 i 个字符全部删除即可
        for(int j = 0; j <= word2.size(); j++)  
            dp[0][j] = j; // word1 为空字符串时,将 word2 的前 j 个字符全部插入即可
        
        // 动态规划求解最小编辑距离
        for(int i = 1; i <= word1.size(); i++) {
            for(int j = 1; j <= word2.size(); j++) {
                if(word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1]; // 当前字符相同,不需要进行操作,取左上角的值
                } else {
                    // 当前字符不同,可以选择插入、删除、替换操作中的最小值,并加上一次操作
                    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
                }
            }
        }
        
        // 返回最小编辑距离
        return dp[word1.size()][word2.size()];
    }
};

3回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

复制代码
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

复制代码
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

思路:

首先,我们定义一个二维数组 dp,其中 dp[i][j] 表示字符串 s 中以索引 i 开始、索引 j 结束的子串是否为回文串。初始化时,我们将所有 dp[i][j] 的值设为 false,表示初始时所有子串都不是回文串。

然后,我们从字符串的末尾开始向前遍历,这样我们可以先计算出小区间的结果,再利用小区间的结果计算大区间的结果。对于每个索引 i,我们再从 i 开始向后遍历,对于每个索引 j,我们判断 s[i] 是否等于 s[j]

  • 如果 s[i] 等于 s[j],则可能存在回文子串。我们进一步分析:
    1. 如果 j - i 小于等于 1,说明子串长度为 1 或 2,此时 s[i]s[j] 相等,所以子串是回文串,我们将结果加一,并将 dp[i][j] 设为 true
    2. 如果 j - i 大于 1,且 dp[i + 1][j - 1]true,说明内部子串是回文串,且 s[i]s[j] 相等,此时整个子串也是回文串,我们将结果加一,并将 dp[i][j] 设为 true

最终,我们返回计算得到的回文子串的数量。

代码

cpp 复制代码
class Solution {
public:
    int countSubstrings(string s) {
        // 创建二维数组 dp,dp[i][j] 表示 s 中以索引 i 开始、索引 j 结束的子串是否为回文串
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int result = 0;
        
        // 从后向前遍历字符串 s,保证先计算出小区间的结果,再利用小区间的结果计算大区间的结果
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) { // 情况一:子串长度为 1 或 2
                        result++; // 单个字符或相邻字符构成的子串都是回文串
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) { // 情况二:子串长度大于 2,且内部子串是回文串
                        result++; // 内部子串是回文串,且两端字符相同,则整个子串也是回文串
                        dp[i][j] = true;
                    }
                }
            }
        }
        
        // 返回回文子串的数量
        return result;
    }
};

4. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

复制代码
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

复制代码
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

  • 1 <= s.length <= 1000
  • s 仅由小写英文字母组成

思路:

  1. 定义状态: 首先,我们需要定义动态规划状态。在这里,我们使用二维数组 dp[i][j],其中 dp[i][j] 表示从字符串 s 的下标 ij 之间的子串中的最长回文子序列长度。

  2. 状态转移方程: 接下来,我们需要找到状态之间的转移关系。当我们在考虑 dp[i][j] 时,我们可以根据 s[i]s[j] 的关系来决定如何更新 dp[i][j] 的值:

    • 如果 s[i] == s[j],那么 dp[i][j] 可以由 dp[i + 1][j - 1] 推导而来,即 dp[i][j] = dp[i + 1][j - 1] + 2,因为两端相同的字符可以增加最长回文子序列的长度。
    • 如果 s[i] != s[j],那么 dp[i][j] 可以由 dp[i + 1][j]dp[i][j - 1] 中的较大值推导而来,因为当前位置的字符不同,所以无法直接形成回文,只能取相邻子串中的最长回文子序列的长度作为当前子串的最长回文子序列长度,即 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
  3. 初始化: 我们需要对动态规划数组进行初始化。对角线上的元素 dp[i][i] 表示单个字符的子串为回文子序列,因此初始化为 1。

  4. 状态转移计算: 我们从字符串的末尾开始向前遍历,逐步计算出每个子串的最长回文子序列长度,直到计算出整个字符串的最长回文子序列长度。

  5. 返回结果: 最后,我们返回 dp[0][s.size() - 1],即整个字符串的最长回文子序列长度

代码:

cpp 复制代码
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        // 创建二维数组 dp,dp[i][j] 表示以下标 i 到 j 之间的子串中的最长回文子序列长度
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        
        // 初始化 dp 数组的对角线,即单个字符的子串为回文子序列,长度为 1
        for (int i = 0; i < s.size(); i++) 
            dp[i][i] = 1;
        
        // 动态规划求解最长回文子序列长度
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2; // 当前字符相同,加上两端字符的最长回文子序列长度
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); // 当前字符不同,取相邻子串中的最长回文子序列长度
                }
            }
        }
        
        // 返回整个字符串的最长回文子序列长度
        return dp[0][s.size() - 1];
    }
};
相关推荐
小码农<^_^>9 分钟前
优选算法精品课--滑动窗口算法(一)
算法
羊小猪~~11 分钟前
神经网络基础--什么是正向传播??什么是方向传播??
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
软工菜鸡37 分钟前
预训练语言模型BERT——PaddleNLP中的预训练模型
大数据·人工智能·深度学习·算法·语言模型·自然语言处理·bert
南宫生40 分钟前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
AI视觉网奇1 小时前
sklearn 安装使用笔记
人工智能·算法·sklearn
JingHongB1 小时前
代码随想录算法训练营Day55 | 图论理论基础、深度优先搜索理论基础、卡玛网 98.所有可达路径、797. 所有可能的路径、广度优先搜索理论基础
算法·深度优先·图论
weixin_432702261 小时前
代码随想录算法训练营第五十五天|图论理论基础
数据结构·python·算法·深度优先·图论
小冉在学习2 小时前
day52 图论章节刷题Part04(110.字符串接龙、105.有向图的完全可达性、106.岛屿的周长 )
算法·深度优先·图论
Repeat7152 小时前
图论基础--孤岛系列
算法·深度优先·广度优先·图论基础
小冉在学习2 小时前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论