LeetCode 剑指 Offer II 动态规划(二) 专题总结

  • 📚 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
  • 👉 欢迎 关注❤️点赞👍收藏⭐️评论📝
  • 😜 小逸白正在备战实习,经常更新面试题LeetCode题解,欢迎志同道合的朋友互相交流~
  • 💙 若有问题请指正,记得关注哦,感谢~

往期文章 :

094. 最少回文分割(困难)

题目:

给定一个字符串 s,请将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。

示例:

输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。

提示:

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

思路:

切割字符串成为回文串需要若干步,如切割 “abb” 需要先考虑 “a” 再考虑 “ab” 最后考虑 “abb”,对于任何一步都面临若干个选择,如切割 “abb” ,最后一个 ‘b’ 既可以组成 “bb” 又可以组成 “b” ,答案需要返回最少的切割次数,所以该问题可以使用动态规划解决。
dp(i) 表示字符串 s[0] ~ s[i] 的最小回文分割次数。若存在 j <= i 使得 s[j] ~ s[i] 为回文串,那么只需要在字符串 s[0] ~ s[j - 1] 后切一次,字符串 s[j] ~ s[i] 便可组成回文串,因为字符串 s[0] ~ s[j - 1] 的最小切割次数为 dp(j - 1) ,故可得 dp(i) = dp(j - 1) + 1。因为可能存在多个 j (起码存在 j = i 使 s[j] ~ s[i] 为单个字符为回文串),最后只需在符合要求的 j 值中取 dp(i) = min (dp[i], dp(j - 1) + 1) 即可。
整个过程需要不断寻找是否存在 j <= i 使得 s[j] ~ s[i] 为回文串,这个过程的时间复杂度为 O(n),所以整个算法的时间复杂度为 O(n^3)。为了优化时间复杂度,可以先判断所有的字符串 s[j] ~ s[i] 是否为回文,并将所有的结果保存在二维数组 isPalindrome[j][i]。具体的做法就是先确定回文的中心,之后从中心开始扩散确定回文的情况。处理之后,整个算法的时间复杂度可以优化为 O(n^2)
完整的代码如下,时间复杂度为 O(n^2),空间复杂度为 O(n^2)

class Solution {
public:
    int minCut(string s) {
        int n = s.length();
        vector<vector<bool>> isPalindrome(n, vector<bool>(n, true));
        vector<int> dp(n, INT_MAX);
        
        // 动态规划确认i ~ j 是否是回文串
        // isPalindrome[i][j]依赖于isPalindrome[i+1][j-1],所以i从大到小,j从i后面开始从小到大
        for(int i = n - 1; i >= 0; i--) {
            for(int j = i + 1; j < n; j++) {
                isPalindrome[i][j] = s[i] == s[j] && isPalindrome[i+1][j-1];
            }
        }
        for(int i = 0; i < n; i++) {
            // 0~i 不用切割, 之前需要切割的话也会从i开始变成0次, dp[i] = 0
            if(isPalindrome[0][i] == true) {
                dp[i] = 0;
            }else {
                // f[i] = 0 ~ i 需要切割的最小次数,从1开始,0 ~ i已经判断过不是回文串
                for(int j = 1; j <= i; j++) {
                    // 如果j ~ i 是回文串就判断 当前值跟前一段再分割一次的最小值
                    if(isPalindrome[j][i]) {
                        dp[i] = min(dp[i], dp[j - 1] + 1);
                    }
                }
            }
        }
        return dp[n - 1];
    }
};

095. 最长公共子序列

题目:

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例:

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

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1text2 仅由小写英文字符组成。

思路:

两个字符串可能存在多个公共子序列,题目要求计算最长公共子序列的长度,因此可以考虑使用动态规划来解决。
用函数 f(i, j) 表示字符串 s1 中下标从 0 开始到 i 的子字符串 s1[0…i] 和字符串 s2 中下标从 0 开始到 j 的子字符串 s2[0…j] 的最长公共子序列的长度。对于 f(i, j),如果 s1[i] == s2[j],那么相当于在 s1[0…i - 1] 和 s2[0…j - 1] 的最长公共子序列的后面添加一个公共字符,也就是 f(i, j) = f(i - 1, j - 1) + 1。如果 s1[i] != s2[j],那么这两个字符不可能出现在 s1[0…i] 和 s2[0…j] 的公共子序列中。此时 s1[0…i] 和 s2[0…j] 的最长公共子序列是s1[0…i - 1] 和 s2[0…j] 的最长公共子序列和s1[0…i] 和 s2[0…j - 1] 的最长公共子序列中的较大值,即 f(i, j) = max(f(i - 1, j), f(i, j - 1))
所以转移状态方程为
s1[i] == s2[j]时,f(i, j) = f(i-1, j-1) + 1
s1[i] != s2[i]时,f(i, j) = max(f(i-1, j), f(i, j-1)
时间复杂度为 O(mn),空间复杂度为 O(mn)

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.length();
        int m = text2.length();
        vector<vector<int>> dp(n+1, vector<int>(m+1));
        for(int i = 0; i <= n; i++) {
            for(int j = 0; j <= m; j++) {
                if(i == 0 || j == 0) {
                    dp[i][j] = 0;
                    continue;
                }
                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[n][m];
    }
};

096. 字符串交织

题目:

给定三个字符串 s1s2s3,请判断 s3 能不能由 s1s2 交织(交错) 组成。
两个字符串 st 交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • 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 意味着字符串 ab 连接。

示例:
在这里插入图片描述
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1s2、和 s3 都由小写英文字母组成

思路:

因为每一步都需要从两个字符串中的不同位置处选择字符,所以需要使用两个变量来确定状态转移方程。设 f(i, j) 表示字符串 s1 的下标从 0 到 i 的子字符串 s1[0] ~ s1[i] (长度为i + 1) 和字符串 s2 的下标从 0 到 j 的子字符串 s2[0] ~ s2[j] (长度为j + 1) 能否交织成字符串 s3 的下标从 0 到 i + j + 1 的子字符串 s3[0] ~ s3[i + j + 1] (长度为i + j + 2)。
根据交织的规则,字符串 s3 下标为 i + j + 1 的字符 s3[i + j + 1] 既可能来自于字符串 s1 的下标为 i 的字符 s1[i],也可能来自于字符串 s2 的下标为 j 的字符 s2[j]。如果 s3[i + j + 1] == s1[i]成立,那么字符 s3[i + j + 1] 可以来自于字符串 s1,那么可以的得到 f(i, j) = f(i - 1, j),也就是说此时只要 s1[0] ~ s1[i - 1] 和 s2[0] ~ s2[j] 可以交织成 s3[0] ~ s3[i + j],那么 s1[0] ~ s1[i] 和 s2[0] ~ s2[j] 也可以交织成 s3[0] ~ s3[i + j + 1]。同理可得,若s3[i + j + 1] == s2[j]成立,那么f(i, j) = f(i, j - 1)。以上两个选择只要其中一个能使 f(i, j)为 true即可。所以可得状态转移方程为
dp[i][j] = ((ch1 == ch3) && dp[i-1][j]) || ((ch2 == ch3) && dp[i][j-1]);
因为存在上式中 i, j >= 0,所以会出现· f(i, -1)· 的情况,这表示字符串 s2 的子串为空时 s1[0] ~ s1[i] 是否可以交织为 s3[0] ~ s3[i],即两者是否相等。同理 ·f(-1, j)· 表示字符串 s1 的子串为空时 s2[0] ~ s2[j] 是否可以交织为 s3[0] ~ s3[j],即两者是否相等。另外 f(-1, -1)表示两个空的字符串是否可以交织成一个空的字符串,这显然成立,所以 f(-1, -1) = true
时间复杂度为 O(mn),空间复杂度为 O(mn)

以 s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac” 为例子,二维 DP 矩阵如下
在这里插入图片描述

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.size(), m = s2.size();
        if(n + m != s3.size()) {
            return false;
        }
        vector<vector<bool>> dp(n+1, vector<bool>(m+1, false));
        dp[0][0] = true;
        for(int i = 0; i < n && s1[i] == s3[i]; i++) {
            dp[i + 1][0] = true;
        }
        for(int j = 0; j < m && s2[j] == s3[j]; j++) {
            dp[0][j + 1] = true;
        }
        // dp[i][j] = ((ch1 == ch3) && dp[i-1][j]) || ((ch2 == ch3) && dp[i][j-1]);
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                char ch1 = s1[i];
                char ch2 = s2[j];
                char ch3 = s3[i + j + 1];
                dp[i + 1][j + 1] = ((ch1 == ch3) && dp[i][j + 1]) || ((ch2 == ch3) && dp[i + 1][j]);
            }
        }
        return dp[n][m];
    }
};
评论 52
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小逸白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值