最长回文子串(暴力枚举、动态规划、中心扩展,leetcode刷题记录)

5. 最长回文子串

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

示例 1:

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

示例 2:

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

提示:

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

思路: 对于这个问题,主要有三种常见解法:
a) 暴力枚举(时间复杂度 O ( n 3 ) O(n^3) O(n3)
b) 动态规划(时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n 2 ) O(n^2) O(n2)
c) 中心扩展法(时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)

**暴力枚举法实现思路:**遍历原始字符串中的所有子串,用两个变量i、j来遍历,其中i表示子串起点,j表示子串终点,子串长度就是j-i+1,i取值从0到n-1;j取值从i到n-1;这样就可以遍历到原始字符串中任意长度、任意位置的子串,然后对每个子串进行是否是回文串的判断,判断思路就是先验证首尾字符是否相等,如果不相等,则不是回文串,如果相等,进一步向内判断次首尾字符是否相等,直到首尾start和end重合,如果都没有找到不相等的,就说明该子串是回文子串;对于是回文子串的,不断记录其长度和起点,并用长度更大的回文子串的长度来更新,这样遍历完所有的子串后就可以得到最长回文子串的长度和起点,然后通过索引原始字符串来返回最长回文子串。

class Solution {
public:
    // 判断字符串是否为回文
    bool isPalindrome(string& s, int start, int end) {
        // 双指针从两端向中间检查
        while (start < end) {
            // 如果首尾字符不同,不是回文
            if (s[start] != s[end]) {
                return false;
            }
            start++;
            end--;
        }
        return true;
    }
    
    string longestPalindrome(string s) {
        int n = s.length();
        // 特殊情况处理
        if (n < 2) return s;
        
        // 初始化最长回文子串
        int maxLen = 1;   // 最小长度为1
        int start = 0;    // 起始位置
        
        // 枚举所有可能的子串(起始位置)
        for (int i = 0; i < n; i++) {
            // 枚举所有可能的子串(结束位置)
            for (int j = i; j < n; j++) {
                // 当前子串长度
                int currLen = j - i + 1;
                
                // 如果当前子串是回文且长度大于已记录的最长回文
                if (currLen > maxLen && isPalindrome(s, i, j)) {
                    // 更新最长回文子串的起始位置和长度
                    maxLen = currLen;
                    start = i;
                }
            }
        }
        
        // 返回最长回文子串
        return s.substr(start, maxLen);
    }
};

**动态规划法实现思路:**关键在于如何构建大问题和子问题的解的关系,大问题如何利用子问题的解简化求解过程,减少一些重复求解过程。这里实现思路有点类似算法导论15.2节矩阵链乘法求最优括号方案,都是用动态规划实现,定义初始二维dp数组,初始化全为false,然后遍历原始字符串中所有长度的子串,从长度为2开始,一直遍历到和原始字符串等长,对每个固定长度len的子串,遍历它在原始字符串中可能的所有位置,如起点i和终点j=i+len-1,对每一个子串,首先判断该子串的首尾字符是否相等,如果不相等,一定不是回文子串,直接令dp[i][j]=fasle,如果首尾字符相同且子串长度小于等于3,就可以直接说明该子串为回文子串,如果该子串长度超过3,则仅根据首尾字符相等无法说明它就是回文子串,此时需要缩短子串长度,起点和终点都像内部移动一格,判断该子串的子串dp[i+1][j-1]是否是回文子串,如果该子串的子串是回文子串,则该子串就是回文子串,这里就将大问题dp[i][j]是否是回文子串的问题分解为更下的子问题:1、s[i]是否等于s[j];2、dp[i+1][j-1]是否为true;只有两者都为“是”,dp[i][j]才为true,要判断dp[i][j]是否为true,就要先判断s[i]是否等于s[j],如果相等,再判断dp[i+1][j-1]是否为true,所以dp[i][j]要从i、j小的时候开始求,对应着更小的子问题,更小的子问题解决了,更大的问题dp[i][j]就可以根据子问题的解来进行判断了。

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        if (n < 2) return s;
        
        // 定义二维dp数组,初始化为false,dp[i][j]用于表示起点为i、终点为j的子串是否是回文子串
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        
        // 记录最长回文子串的起始位置和长度
        int maxLen = 1;   // 最小长度为1
        int start = 0;    // 起始位置
        
        // 单个字符都是回文
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        
        // 从长度为2的子串开始遍历
        for (int len = 2; len <= n; len++) {
            // 枚举子串的起始位置
            for (int i = 0; i < n - len + 1; i++) {
                // 计算子串结束位置
                int j = i + len - 1;
                
                // 检查首尾字符是否相同
                if (s[i] != s[j]) {
                    dp[i][j] = false;
                } 
                else {
                    // 长度 <= 3 直接判定为回文
                    if (len <= 3) {
                        dp[i][j] = true;
                    } 
                    else {
                        // 长度 > 3,需要检查内部子串
                        dp[i][j] = dp[i+1][j-1];
                    }
                }
                
                // 更新最长回文子串
                if (dp[i][j] && len > maxLen) {
                    maxLen = len;
                    start = i;	    // 根据最长回文子串的起始位置和长度,在return时候通过索引获取最长回文字符串
                }
            }
        }
        
        // 返回最长回文子串
        return s.substr(start, maxLen);
    }
};

中心扩展法实现思路: 遍历给定字符串的所有字符,以所有字符为中心,分奇数长度回文、偶数长度回文依次向左右两侧扩展,如果左右两侧不超过字符串界限且 扩展的左右两侧字符相等(说明构成回文序列),则继续扩展,如果不相等,则返回当前回文序列字符串,然后继续以下一个字符为中心,继续以相同的方式向左右两侧扩展,直到不满足回文条件或超出字符串边界,遍历完成后,最长的回文序列字符串就是原始字符串中的最长回文子串,具体 C++实现代码如下:

// 中心扩展法
class Solution {  
public:  
    // 中心扩展辅助函数  
    string expandAroundCenter(string& s, int left, int right) {  
        // 当左右指针未越界且字符相同时继续扩展  
        while (left >= 0 && right < s.length() && s[left] == s[right]) {  
            left--;  
            right++;  
        }  
        // 返回找到的回文子串(注意需要调整left和right)  
        return s.substr(left + 1, right - left - 1);  
    }  
    
    string longestPalindrome(string s) {  
        if (s.length() < 2) return s;  // 如果初始给的字符串长度为1,直接返回
        
        string longest = "";    // 初始化最长回文字符串为空
        
        // 遍历每个可能的回文中心  
        for (int i = 0; i < s.length(); i++) {  
            // 奇数长度回文(以单个字符为中心)  
            string odd = expandAroundCenter(s, i, i);  
            // 偶数长度回文(以两个字符之间为中心)  
            string even = expandAroundCenter(s, i, i + 1);  
            
            // 更新最长回文子串,奇数长度回文和偶数长度回文,哪个长哪个就是longest  
            if (odd.length() > longest.length()) longest = odd;  
            if (even.length() > longest.length()) longest = even;  
        }  
        
        return longest;  
    }  
};  

你可能感兴趣的:(LeetCode刷题记录,动态规划,leetcode,算法)