[每日一题] 119. 最长回文子串(字符串、动态规划、马拉车算法、多方法)

1. 题目来源

链接:最长回文子串
来源:LeetCode

2. 题目说明

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

示例1:

输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。

示例1:

输入: “cbbd”
输出: “bb”

3. 题目解析

方法一:利用子函数朴素遍历求法

这道题让求最长回文子串,传统的验证回文串的方法就是两个两个的对称验证是否相等。

那么对于找回文子串的问题,就要以每一个字符为中心,像两边扩散来寻找回文串,这个算法的时间复杂度是 O(n * n), 可以通过 OJ,就是要注意奇偶情况,由于回文串的长度可奇可偶,比如 “bob” 是奇数形式的回文,“noon” 就是偶数形式的回文,两种形式的回文都要搜索

  • 对于奇数形式的,就从遍历到的位置为中心,向两边进行扩散
  • 对于偶数形式的,就把当前位置和下一个位置当作偶数行回文的最中间两个字符,然后向两边进行搜索

参见代码如下:

// 执行用时 :48 ms, 在所有 C++ 提交中击败了68.87%的用户
// 内存消耗 :104.1 MB, 在所有 C++ 提交中击败了26.91%的用户

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 了,跟上面的操作一样

参见代码如下:

// 执行用时 :8 ms, 在所有 C++ 提交中击败了96.95%的用户
// 内存消耗 :8.9 MB, 在所有 C++ 提交中击败了79.79%的用户

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);
    }
};
方法三:动规解法

此题还可以用动态规划来解,维护一个二维数组 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] = 1                                   if i == j

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

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

这里有个有趣的现象就是如果把下面的代码中的二维数组由 int 改为 vector 后,就会超时,这说明 int 型的二维数组访问执行速度完爆 std 的 vector 啊,所以以后尽可能的还是用最原始的数据类型吧。

// 执行用时 :292 ms, 在所有 C++ 提交中击败了18.00%的用户
// 内存消耗 :13.1 MB, 在所有 C++ 提交中击败了58.43%的用户

class Solution {
public:
    string longestPalindrome(string s) {
        if (s.empty()) return "";
        int n = s.size(), dp[n][n] = {0}, left = 0, len = 1;
        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);
    }
};
方法四:Manacher’s Algorithm 马拉车算法解法

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

// 执行用时 :8 ms, 在所有 C++ 提交中击败了96.95%的用户
// 内存消耗 :9.2 MB, 在所有 C++ 提交中击败了76.59%的用户

class Solution {
public:
    string longestPalindrome(string s) {
        string t ="$#";
        for (int i = 0; i < s.size(); ++i) {
            t += s[i];
            t += '#';
        }
        int p[t.size()] = {0}, 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);
    }
};

你可能感兴趣的:(每日一题,算法编程题,字符串,动态规划,马拉车算法,多方法)