Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LeetCode] 5. Longest Palindromic Substring #5

Open
grandyang opened this issue May 30, 2019 · 4 comments
Open

[LeetCode] 5. Longest Palindromic Substring #5

grandyang opened this issue May 30, 2019 · 4 comments

Comments

@grandyang
Copy link
Owner

grandyang commented May 30, 2019


请点击下方图片观看讲解视频
Click below image to watch YouTube Video
Video

Given a string s, return the longest palindromic substring in s.

Example 1:

**Input:** s = "babad"
**Output:** "bab"
**Explanation:** "aba" is also a valid answer.

Example 2:

**Input:** s = "cbbd"
**Output:** "bb"

Constraints:

  • 1 <= s.length <= 1000
  • s consist of only digits and English letters.

这道题让我们求最长回文子串,首先说下什么是回文串,就是正读反读都一样的字符串,比如 "bob", "level", "noon" 等等。那么最长回文子串就是在一个字符串中的那个最长的回文子串。LeetCode 中关于回文串的题共有五道,除了这道,其他的四道为 Palindrome NumberValidate PalindromePalindrome PartitioningPalindrome Partitioning II,我们知道传统的验证回文串的方法就是两个两个的对称验证是否相等,那么对于找回文字串的问题,就要以每一个字符为中心,像两边扩散来寻找回文串,这个算法的时间复杂度是 O(n*n),可以通过 OJ,就是要注意奇偶情况,由于回文串的长度可奇可偶,比如 "bob" 是奇数形式的回文,"noon" 就是偶数形式的回文,两种形式的回文都要搜索,对于奇数形式的,我们就从遍历到的位置为中心,向两边进行扩散,对于偶数情况,我们就把当前位置和下一个位置当作偶数行回文的最中间两个字符,然后向两边进行搜索,参见代码如下:

解法一:

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.size() < 2) return s;
        int n = s.size(), maxLen = 0, start = 0;
        for (int i = 0; i < n - 1; ++i) {
            searchPalindrome(s, i, i, start, maxLen);
            searchPalindrome(s, i, i + 1, start, maxLen);
        }
        return s.substr(start, maxLen);
    }
    void searchPalindrome(string s, int left, int right, int& start, int& maxLen) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            --left; ++right;
        }
        if (maxLen < right - left - 1) {
            start = left + 1;
            maxLen = right - left - 1;
        }
    }
};

我们也可以不使用子函数,直接在一个函数中搞定,我们还是要定义两个变量 start 和 maxLen,分别表示最长回文子串的起点跟长度,在遍历s中的字符的时候,我们首先判断剩余的字符数是否小于等于 maxLen 的一半,是的话表明就算从当前到末尾到子串是半个回文串,那么整个回文串长度最多也就是 maxLen,既然 maxLen 无法再变长了,计算这些就没有意义,直接在当前位置 break 掉就行了。否则就要继续判断,我们用两个变量 left 和 right 分别指向当前位置,然后我们先要做的是向右遍历跳过重复项,这个操作很必要,比如对于 noon,i在第一个o的位置,如果我们以o为最中心往两边扩散,是无法得到长度为4的回文串的,只有先跳过重复,此时left指向第一个o,right指向第二个o,然后再向两边扩散。而对于 bob,i在第一个o的位置时,无法向右跳过重复,此时 left 和 right 同时指向o,再向两边扩散也是正确的,所以可以同时处理奇数和偶数的回文串,之后的操作就是更新 maxLen 和 start 了,跟上面的操作一样,参见代码如下:

解法二:

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.size() < 2) return s;
        int n = s.size(), maxLen = 0, start = 0;
        for (int i = 0; i < n;) {
            if (n - i <= maxLen / 2) break;
            int left = i, right = i;
            while (right < n - 1 && s[right + 1] == s[right]) ++right;
            i = right + 1;
            while (right < n - 1 && left > 0 && s[right + 1] == s[left - 1]) {
                ++right; --left;
            }
            if (maxLen < right - left + 1) {
                maxLen = right - left + 1;
                start = left;
            }
        }
        return s.substr(start, maxLen);
    }
};

此题还可以用动态规划 Dynamic Programming 来解,根 Palindrome Partitioning II 的解法很类似,我们维护一个二维数组 dp,其中 dp[i][j] 表示字符串区间 [i, j] 是否为回文串,当 i = j 时,只有一个字符,肯定是回文串,如果 i = j + 1,说明是相邻字符,此时需要判断 s[i] 是否等于 s[j],如果i和j不相邻,即 i - j >= 2 时,除了判断 s[i] 和 s[j] 相等之外,dp[i + 1][j - 1] 若为真,就是回文串,通过以上分析,可以写出递推式如下:

dp[i, j] = true if i == j

= s[i] == s[j] if j = i + 1

= s[i] == s[j] && dp[i + 1][j - 1] if j > i + 1

解法三:

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.empty()) return "";
        int n = s.size(), left = 0, len = 1;
        vector<vector<bool>> dp(n, vector<bool>(n));     
        for (int i = 0; i < n; ++i) {
            dp[i][i] = true;
            for (int j = 0; j < i; ++j) {
                dp[j][i] = (s[i] == s[j] && (i - j < 2 || dp[j + 1][i - 1]));
                if (dp[j][i] && len < i - j + 1) {
                    len = i - j + 1;
                    left = j;
                }
            }
        }
        return s.substr(left, len);
    }
};

最后要来的就是大名鼎鼎的马拉车算法 Manacher's Algorithm,这个算法的神奇之处在于将时间复杂度提升到了 O(n) 这种逆天的地步,而算法本身也设计的很巧妙,很值得我们掌握,参见我另一篇专门介绍马拉车算法的博客 Manacher's Algorithm 马拉车算法,代码实现如下:

解法四:

class Solution {
public:
    string longestPalindrome(string s) {
        string t ="$#";
        for (int i = 0; i < s.size(); ++i) {
            t += s[i];
            t += '#';
        }
        vector<int> p(t.size());
        int id = 0, mx = 0, resId = 0, resMx = 0;
        for (int i = 1; i < t.size(); ++i) {
            p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
            while (t[i + p[i]] == t[i - p[i]]) ++p[i];
            if (mx < i + p[i]) {
                mx = i + p[i];
                id = i;
            }
            if (resMx < p[i]) {
                resMx = p[i];
                resId = i;
            }
        }
        return s.substr((resId - resMx) / 2, resMx - 1);
    }
};

Github 同步地址:

#5

类似题目:

Shortest Palindrome

Palindrome Permutation

Palindrome Pairs

Longest Palindromic Subsequence

Palindromic Substrings

参考资料:

https://leetcode.com/problems/longest-palindromic-substring/

https://leetcode.com/problems/longest-palindromic-substring/discuss/2928/Very-simple-clean-java-solution

https://leetcode.com/problems/longest-palindromic-substring/discuss/2923/Simple-C%2B%2B-solution-(8ms-13-lines)

https://leetcode.com/problems/longest-palindromic-substring/discuss/2921/Share-my-Java-solution-using-dynamic-programming

LeetCode All in One 题目讲解汇总(持续更新中...)

(欢迎加入博主的知识星球,博主将及时答疑解惑,并分享刷题经验与总结,快快加入吧~)

知识星球 喜欢请点赞,疼爱请打赏❤️~.~

微信打赏

|

Venmo 打赏


---|---

@lld2006
Copy link

lld2006 commented Oct 8, 2019

1, 加上#号后,最左边和最右边都是#号, 总长度是2*radius - 1, 其中 #个数是 radius, 字符的长度是radius-1;
2, 中心是 center,左边 右边各有radius-1 个字符, 所以最左边的字符(不包含#号)的index是center-(radius-1)+1 但是字符的index 是2 4 6 8 ..., 所以最长回文字符串最左边的字符在不包含#号的index就是 (center-radius+2-2)/2;

这样就不需要归纳了,

@conghao321
Copy link

这个题的DP解法 用C++报错了老哥

@znculee
Copy link

znculee commented Oct 20, 2020

这个题的DP解法 用C++报错了老哥

@conghao321 @grandyang 问题在于dp数组的0初始化。如下改用memset就好了,或者干脆直接不初始化这道题也没问题。

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.empty()) return "";
        int n = s.length(), left = 0, len = 1;
        int dp[n][n];
        memset(dp, 0, sizeof dp);
        for (int i = 0; i < n; ++i) {
            dp[i][i] = 1;
            for (int j = 0; j < i; ++j) {
                dp[j][i] = (s[i] == s[j] && (i - j < 2 || dp[j + 1][i - 1]));
                if (dp[j][i] && len < i - j + 1) {
                    len = i - j + 1;
                    left = j;
                }
            }
        }
        return s.substr(left, len);
    }
};

@Nannew
Copy link

Nannew commented Aug 18, 2021

解法三 只用了一个bool 没必要用int

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.empty()) {
            return "";
        }
        int n = s.size(), left = 0, len = 1;
        bool dp[n][n];
        memset(dp,0,n*n*sizeof(bool));
        //vector<vector<bool>> dp(n, vector<bool>(n,0));
        for (int i = 0; i < n; ++i) {
            dp[i][i] = true;
            for (int j = 0; j < i; ++j) {
                dp[j][i] = (s[i] == s[j]) && (i - j < 2 || dp[j + 1][i - 1]);
                if(dp[j][i] && len < i - j + 1) {
                    len = i - j + 1;
                    left = j;
                }
            }
        }
        return s.substr(left, len);
    }
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants