LeetCode5 最长回文子串------中等

题目:
给定一个字符串 s,找到 s 中最长的回文子串你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:

输入: "cbbd"
输出: "bb"

注意
1、子串和序列的区别
在这里插入图片描述
2、假设 s 的最大长度为 1000
告诉我们设计一个时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)的算法是可以的(为啥??????????)
3、
从示例中看出,只需输出一个回文子串

关键点
由于截取字符串有一点的性能消耗,一个等价的方式是:记录最长回文子串的起止下标和它的长度,在最后输出的时候再做截取就可了。

一、暴力解法(枚举左右边界)

class Solution:
    def longestPalindrome(self, s: str) -> str:
        l = len(s)
        if l==0:
            return ""
        l_max = 0
        s_max = ""
        for i in range(l):#左边界
            for j in range(i+1,l+1):#注意右边界从左边界后一个开始,到l+1,因为右边界需取到串的最后一个元素,而(i+1,l)只能取到l-1
                s_chlid = s[i:j]
                if s_chlid == s_chlid[::-1]:
                    l_chlid = len(s_chlid)
                    if l_chlid > l_max:
                        s_max = s_chlid
                    l_max = max(l_chlid,l_max)
        return s_max

在这里插入图片描述

在两个for循环(n^2^)里还有一个遍历(即,判断是否是回文),所以时间复杂度是O(n^3^)

在这里插入图片描述

二、动态规划(暴力解法的优化)
1 .关键点
回文串是天然具有状态转移性质的,即,回文串去掉两头后还是回文串(这里回文串的长度要严格大于2)。即,如果一个子串两头若不想等,我们可以直接下结论此子串不是回文串,若两头相等,则用相同的方法继续判断剩下的子串是否是回文。
也就是说,在子串两头相等的情况下,此子串是否是回文串,取决于去掉两头后的子串部分是否是回文。
因此,可将状态定义成子串是否是回文:

--状态:dp[i][j],表示子串s[i;j]是否是回文串。//注:这里s是闭区间,包括下表i,j
--得到状态转移方程:dp[i][j] = (s[i] = s[j] and dp[i+1][j-1]//s[i;j]状态为true的条件是(即s[i;j]是回文串的条件):在s两端相等的情况下,其子串dp[i+1][j-1]是回文串
既然是下表访问,就得考虑边界的条件:
--边界条件:(j-1-(i+1+1<2,即j-i<3 即j-i+1<4(计算字符串长度的公式)//s[i;j]长度为2或3时不用检查子串是否是回文。
动态规划比暴力解法快就快在了:我们利用状态转移方程快速的得到一个子串是否是回文串,每一步的计算都尽可能的利用了之前计算的结果,这也是非i常典型的空间换时间的思想
--初始化:dp[i][i]==true//单个字符一定是回文串,所以对角线的值可以先赋值为true,由于填表时,并没有用到对角线的值,所以也可不用赋此值。
--输出:在得到一个状态的值为true的时候,记录其实位置和长度,填表完成以后在截取。

2. 示例
题目
判断以下字符串是否是回文串
在这里插入图片描述
状态方程

状态转移方程:dp[i][j] = (s[i] = s[j]and(j-i<3 or dp[i+1][j-1]//当j-i<3时,子串的长度为2或3,在s[i] = s[j]的条件下,此子串一定是回文串

二维表格
动态规划实际上是在填写一张二维表格,由于i<=j(左边界 i 要小于等于右边界 j),所以只用填表格的右上角。
在这里插入图片描述

			这个表格记录了s所有子串的状态,因为单个字符一定是回文串,所以对角线是true。
			由状态方程可知,:dp[i][j]参考dp[i+1][j-1](即表格中它左下方的值),所以填表顺序为:
					先升序填列;在升序填行。(沿着行升序的方向,按升序一次填一列,如下图)

在这里插入图片描述
注:
由于填表时,并没有用到对角线的值,所以对角线也可不赋值。
动态规划题的思路
从一个比较小规模的问题开始,逐步得到较大规模问题的结果,并且在这个过程中记录每一步的结果。
代码
java

class Solution {
    
    
    public String longestPalindrome(String s) {
    
    
        int len = s.length();
        // 特殊情况判段
        if (len < 2){
    
    
            return s;
        }

        int maxLen = 1;
        int begin  = 0;

        // 1. 状态定义
        // dp[i][j] 表示s[i...j] 是否是回文串


        // 2. 初始化
        boolean[][] dp = new boolean[len][len];
        for (int i = 0; i < len; i++) {
    
    
            dp[i][i] = true;
        }

        char[] chars = s.toCharArray();
        // 3. 状态转移
        // 注意:先填左下角
        // 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
        for (int j = 1;j < len;j++){
    
    
            for (int i = 0; i < j; i++) {
    
    
                // 头尾字符不相等,不是回文串
                if (chars[i] != chars[j]){
    
    
                    dp[i][j] = false;
                }else {
    
    
                    // 相等的情况下
                    // 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串
                    if (j - i < 3){
    
    
                        dp[i][j] = true;
                    }else {
    
    
                        // 状态转移
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
                // 此时更新记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen){
    
    
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        // 4. 返回值
        return s.substring(begin,begin + maxLen);
    }
}

c++

class Solution {
    
    
public:
    string longestPalindrome(string s) {
    
    
        int len = s.size();
        if(len<2)
        {
    
    
            return s;
        } 
        int maxLen = 1;
        int begin = 0;

        vector<vector<int>> dp(len,vector<int>(len));//1、创建表
        for(int i=0;i<len;i++){
    
    
            dp[i][i]=1;
        }
        for(int j=1;j<len;j++){
    
    
            for(int i=0;i<j;i++){
    
    //2、依据状态转移方程填表
                if (s[i]!=s[j]){
    
    
                    dp[i][j]=0;
                }
                else{
    
    
                    if(j-i<3){
    
    
                        dp[i][j]=1;
                    }
                    else{
    
    
                        dp[i][j]=dp[i+1][j-1];
                    }
                }
                if(dp[i][j]==1 && j-i+1>maxLen){
    
    
                   maxLen = j-i+1;
                   begin = i;
                }

            }  
        } 
    return s.substr(begin,maxLen);//s.substr从表索引begin开始,截取长度为maxLen
    }
};


--时间复杂度: O(n2),n为字符串的长度

动态规划的解法依然是枚举左右边界,只不过在两层for循环后,判断子串是否是回文 所用的动态规划 的时间复杂度是O(1)【因为判断s[i:j]是否是回文可由dp[i + 1][j - 1]直接得到,所以时间复杂度是01)】,
而在暴力枚举中在两层for循环后,判断子串是否是回文 所用的遍历 的时间复杂度是O(n),所以这道题动态规划是暴力枚举的优化

--空间复杂度:O(n2)

由上表得,一共n^2^个表格,一个表格代表一个变量,用了一半,即用了n^2^/2个变量,即空间复杂度:O(n^2^)

在这里插入图片描述

三、中心扩散法
回文串的枚举可以从两边开始(即方法一,枚举各个子串的左右边界),也可以从中间位置开始,我们把枚举(各子串)中心的方法称为“中心扩散法”。
在这里插入图片描述
C++

class Solution {
    
    
public:
    pair<int, int> expandAroundCenter(const string& s, int left, int right) {
    
    
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
    
    
            --left;
            ++right;
        }
        return {
    
    left + 1, right - 1};
    }

    string longestPalindrome(string s) {
    
    
        int start = 0, end = 0;
        for (int i = 0; i < s.size()-1; ++i) {
    
    //回文中心只需枚举到倒数第二位s.size()-2,最后一位不需枚举,因为向右没有字符了,枚举不了了
        	//回文串的长度可能是奇数,也可能是偶数,所以其中心可能是一个也可能是两个,即,下面的两种情况。
            auto [left1, right1] = expandAroundCenter(s, i, i);//长度是奇数,则中心是一个,left1=right1=i
            auto [left2, right2] = expandAroundCenter(s, i, i + 1);//长度是偶数,则中心是两个,left2=i ,right2=i+1
            if (right1 - left1 > end - start) {
    
    
                start = left1;
                end = right1;
            }
            if (right2 - left2 > end - start) {
    
    
                start = left2;
                end = right2;
            }
        }
        return s.substr(start, end - start + 1);
    }
};

python

class Solution:
    def expandAroundCenter(self, s, left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return left + 1, right - 1

    def longestPalindrome(self, s: str) -> str:
        start, end = 0, 0
        for i in range(len(s)-1):
            left1, right1 = self.expandAroundCenter(s, i, i)
            left2, right2 = self.expandAroundCenter(s, i, i + 1)
            if right1 - left1 > end - start:
                start, end = left1, right1
            if right2 - left2 > end - start:
                start, end = left2, right2
        return s[start: end + 1]

在这里插入图片描述
枚举中心位置的个数2(n-1):因为枚举到倒数第二个,最后一个不枚举,奇数中心和偶数中心情况都是n-1次,所以枚举中心位置的个数是2(n-1)。

猜你喜欢

转载自blog.csdn.net/qq_42647047/article/details/108652634