_49LeetCode代码随想录算法训练营第四十九天-动态规划 | 647.回文子串、516.最长回文子序列

_49LeetCode代码随想录算法训练营第四十九天-动态规划 | 647.回文子串、516.最长回文子序列

题目列表

  • 647.回文子串
  • 516.最长回文子序列
  • 动态规划总结篇

647.回文子串

代码随想录地址:https://programmercarl.com/0647.%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2.html

题目

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

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

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

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

示例 1:

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

示例 2:

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

提示:

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

思路

动态规划思路

个人认为不好理解。

咬死了只有两种基础情况,也就是i和j指向同一个元素,第二个是i和j只相差1,并且s[i] == s[j]。

动规五部曲:

  1. 确定dp数组(dp table)以及下标的含义

观察回文串的性质,如下图:
_49LeetCode代码随想录算法训练营第四十九天-动态规划 | 647.回文子串、516.最长回文子序列_第1张图片

如果 s[1],s[2],s[3] 是回文的,只需要比较s[0]和s[4]元素是否相同,相同的话字符串S就是回文。

找到一种递归关系,判断一个子字符串(字符串的下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。

将dp数组是要定义为二维dp数组。

布尔类型的dp[i] [j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i] [j]为true,否则为false。

  1. 确定递推公式

当s[i]与s[j]不相等,dp[i] [j]一定是false。

当s[i]与s[j]相等时,有三种情况

  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1] [j - 1]是否为true。

因此递推公式如下:

if (s[i] == s[j]) {
    if (j - i <= 1) { // 情况一 和 情况二
        result++;
        dp[i][j] = true;
    } else if (dp[i + 1][j - 1]) { // 情况三
        result++;
        dp[i][j] = true;
    }
}

result就是统计回文子串的数量。

此处没有考虑s[i]与s[j]不相等的时候,因为在初始化dp[i] [j]的时候,就初始为false。

  1. dp数组如何初始化

所以dp[i] [j]初始化为false。

  1. 确定遍历顺序

遍历顺序根据递推公式决定,由于dp[i] [j]与dp[i + 1] [j - 1]相关,因此遍历顺序必须是从下到上、从左到右。

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) { // 情况一 和 情况二
                result++;
                dp[i][j] = true;
            } else if (dp[i + 1][j - 1]) { // 情况三
                result++;
                dp[i][j] = true;
            }
        }
    }
}
  1. 举例推导dp数组

草稿纸。

双指针思路

中心点向外扩散的思想:

在遍历中心点的时候,要注意中心点有两种情况

一个元素可以作为中心点,两个元素也可以作为中心点。

三个元素是由一个元素为中心点扩散而来;四个元素为中心点是由两个元素为中心点扩散而来。

代码

动态规划代码

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)
/*
 * @lc app=leetcode.cn id=647 lang=cpp
 *
 * [647] 回文子串
 */

// @lc code=start
class Solution {
public:
    int countSubstrings(string s) {
        int res = 0;//记录回文子串的个数
        //定义和初始化dp数组
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        //遍历
        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)
                    {
                        res++;
                        dp[i][j] = true;
                    }
                    else if(dp[i + 1][j - 1])
                    {
                        res++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return res;
    }
};
// @lc code=end

双指针思路

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
/*
 * @lc app=leetcode.cn id=647 lang=cpp
 *
 * [647] 回文子串
 */

// @lc code=start
class Solution {
private:
    int extend(string& s, int i, int j, int n)
    {
        int res = 0;
        while(i >= 0 && j < n && s[i] == s[j])
        {
            i--;
            j++;
            res++;
        }
        return res;
    }
public:
    int countSubstrings(string s) {
        int res = 0;
        for(int i = 0; i < s.size(); i++)
        {
            res += extend(s, i, i, s.size());//以一个点为中心
            res += extend(s, i, i + 1, s.size());//以两个点为中心
        }
        return res;
    }
};
// @lc code=end

5.最长回文子串

题目

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

提示:

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

思路

双指针的思路,和 647.回文子串 的双指针思路有点像。

代码

/*
 * @lc app=leetcode.cn id=5 lang=cpp
 *
 * [5] 最长回文子串
 */

// @lc code=start
class Solution {
    private:
    //返回最长回文子串
    int extend(string& s, int i, int j, int n)
    {
        int ex = 0;
        while(i >= 0 && j < n && s[i] == s[j])
        {
            i--;
            j++;
            ex++;
        }
        return ex;
    }
public:
    string longestPalindrome(string s) {
        //记录最长回文子串的起始位置和终止位置,是前闭后闭区间
        int begin = 0;
        int end = 0;
        for(int i = 0; i < s.size(); i++)
        {
            //一个中心节点开始扩散
            int temp = extend(s, i, i, s.size());
            if(temp * 2 - 1 > end - begin + 1)
            {
                begin = i - (temp - 1);
                end = i + (temp - 1);
            }
            //两个中心节点开始扩散
            temp = extend(s, i, i + 1, s.size());
            if(temp * 2 > end - begin + 1)
            {
                begin = i - temp + 1;
                end = i + temp;
            }
        }
        return string(s.begin() + begin, s.begin() + end + 1);
    }
};
// @lc code=end

516.最长回文子序列

代码随想录地址:https://programmercarl.com/0516.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E5%BA%8F%E5%88%97.html

这个题目说明初始化很重要。

题目

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

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

示例 1:

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

示例 2:

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

提示:

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

思路

  1. 确定dp数组(dp table)以及下标的含义

dp[i] [j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i] [j]

  1. 确定递推公式

s[i] == s[j],那么dp[i] [j] = dp[i + 1] [j - 1] + 2;

如图: 516.最长回文子序列

s[i] != s[j]:可以加入s[i]不加入s[j],也可以加入s[j]不加入s[i],看哪个更大。

dp[i] [j] = max(dp[i + 1] [j], dp[i] [j - 1]);

516.最长回文子序列1

  1. dp数组如何初始化

首先要考虑当i 和j 相同的情况,从递推公式:dp[i] [j] = dp[i + 1] [j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。

所以需要手动初始化一下,当i与j相同,那么dp[i] [j]一定是等于1的,即:一个字符的回文子序列长度就是1。

其他情况dp[i][j]初始为0就行,这样递推公式中dp[i] [j]才不会被初始值覆盖。

  1. 确定遍历顺序

根据递推公式,从下到上,从左到右。

  1. 举例推导dp数组

草稿纸。

代码

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)
/*
 * @lc app=leetcode.cn id=516 lang=cpp
 *
 * [516] 最长回文子序列
 */

// @lc code=start
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        //定义及初始化数组
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for(int i = 0; i < s.size(); i++)
            dp[i][i] = 1;
        //遍历
        //j一定要在i的右侧
        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][j - 1], dp[i + 1][j]);
            }
        return dp[0][s.size() - 1];
    }
};
// @lc code=end

动态规划总结篇

代码随想录地址:https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%80%BB%E7%BB%93%E7%AF%87.html

你可能感兴趣的:(Algorithms,算法,动态规划,leetcode,c++)