LeetCode - Longest Palindromic Substring

来源:互联网 发布:药水哥网络臭要饭的 编辑:程序博客网 时间:2024/06/14 02:38

4. Longest Palindromic Substring

原题

Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest substring.

测试程序

我们先给出最长回文串的测试用例程序:

#include <iostream>#include <string>#include <vector>using namespace std;int main(){    string tests[] = {        "",        "a",        "aa",        "abc",        "aba",        "abba",        "abad",        "abeb",        "$#aba#$cd",        "ccccccccccccccccccccccccccccccccccccccccccccccccccccccc"    };    string results[] = {        "",        "a",        "aa",        "a",        "aba",        "abba",        "aba",        "beb",        "$#aba#$",        "ccccccccccccccccccccccccccccccccccccccccccccccccccccccc"    };    int total = sizeof(tests) / sizeof(tests[0]);    cout <<total <<" tests" <<endl;    for (int i = 0; i < total; ++i) {        string palindrome = longest_palindromic_substring(tests[i]);        if (results[i] == palindrome) {            cout <<i <<": test ok" <<endl;        } else {            cout <<i <<": test failed" <<endl;        }    }    return 0;}

备注:"abc"中最长回文字符串可以是"a","b","c",测试程序中使用第一个"a"

朴素(暴力)算法

首先,最容易想到的就是遍历整个字符串S的所有子串,并判断其是否为回文子串,若是则判断其长度是否为最长的回文字符。字符串子串遍历的时间复杂度为O(n2),而判断回文的运行时间为O(n),因此整个算法的运行时间为O(n3)。该算法示例代码如下:

bool is_palindrome(char *s, int len){    char *e = s + len - 1;    while (s < e) {        if (*s != *e) {            return false;        }        s++, e--;    }    return true;    }string longest_palindromic_substring(string s){    int len = s.length();    if (len <= 1) {        return s;    }    int max_len = 0;    char *max_str = NULL;    for (int i = 0; i < len; ++i) {        for (int j = i; j < len; ++j) {            if (is_palindrome(&s[i], j - i + 1)) {                max_len = max_len >= j - i + 1 ? max_len : (max_str = &s[i], j - i + 1);            }        }    }    return string(max_str, max_len);    }

中心扩展算法

上面的代码运行时间高达O(n3),因此对于比较大的字符串来说,其运行时间是比较难接受的。然而,仔细思考一下就能发现回文字符串满足从前到后和从后道前读都是一致的,因此,我们可以利用这个特性对上面的算法进行改进。

  1. 遍历字符串,以当前字符作为中点,向左右两边扩展,当出现不相等的字符时,即回文截至。
  2. 在遍历过程中保存最大长度回文字符串起点及长度。
  3. 由于回文字符串的长度可能为奇数或偶数,因此需要针对奇偶长度不同分别判断。

从上面的步骤可以看到,改进后的方法仅需遍历一次字符串,时间复杂度为O(n),而回文判断的时间复杂度也为O(n),因此总体运行时间为O(n2)。该算法示例代码如下:

string expand(string s, int l, int r){    int len = s.length();    while (l >= 0 && r < len && s[l] == s[r]) {        l--, r++;    }    /* r - l - 1 means r - 1 - (l + 1) + 1 */    return s.substr(l + 1, r - l - 1);}string longest_palindromic_substring(string s){    int len = s.length();    string longest, tmp;    for (int i = 0; i < len; ++i) {        tmp = expand(s, i, i);        if (longest.length() < tmp.length()) {            longest = tmp;        }        tmp = expand(s, i, i + 1);        if (longest.length() < tmp.length()) {            longest = tmp;        }    }    return longest;}

Manacher算法

接下来介绍经典的Manacher算法,该算法利用O(n)的辅助空间,将时间复杂度将到了O(n)。该算法首先通过预处理将上述方法二中的需要针对奇偶长度进行计算统一起来,其次它充分利用了回文的特性通过动态规划的思想避免了不必要的计算。

  1. 预处理:在该阶段,它为每个字符两端插入一个特殊的字符,通常使用'#'字符。如下所示:

    primitive: “adcabacdef”
    processed: “#a#d#c#a#b#a#c#d#e#f#”
    primitive: “abbad”
    processed: “#a#b#b#a#d#”

    因此,无论原始字符串长度是为奇数还是偶数,经预处理之后其长度均为奇数。这样就避免了考虑偶数的情况。由于在类C语言中字符串末尾包含'\0'字符,因此通常还会在字符串的开始加入一个特殊字符'$'避免越界。

  2. 计算回文半径: 回文半径即从回文中心点到回文最左边或最右边的距离。例如上面的示例中,我们可以计算每个字符的回文半径,如下所示:

    primitive: “adcabacdef”
    processed: # a # d # c # a # b # a # c # d # e # f #
    radiuses[-]: 1 2 1 2 1 2 1 2 1 8 1 2 1 2 1 2 1 3 1 2 1

    上述中的空格是为了显示而加上的,rediuses数组中的'-'字符同样是为了显示效果而加上的。可以看出radiuses[i] - 1既是原字符串中回文串的长度。这其实是可以证明的。

    证明:

    1. 首先有L=2radiuses[i]1为新串中以processed[i]为中心的回文串的长度。
    2. processed[i]为中心的回文串一定是以'#'字符开始和结尾的,因此,当L减去回文串最前(或最后)的'#'字符后,其长度正好是原字符串的两倍,即(L1)/2,将L代入化解即得radiuses[i]1

    因此只要计算出预处理后字符串的回文半径数组就可以求出回文字符串了。Manacher算法在计算回文半径是利用到了动态规划的思路。它利用两个辅助变量max_rightmax_index来计算radiuses数组。其中max_radius代表预处理串中最大的回文字符串的最右边位置,max_index则代表回文字符串的中心索引位置(该算法也可以只使用一个max_index辅助变量)。步骤如下:

    1. i<max_right意味着当前字符处于最大回文半径范围内,因此可以利用回文的特性找到其以max_index为中心的对称点(max_index(imax_index)=2max_indexi)的回文半径,避免重复计算。

      1. 若对称点的回文半径小于max_righti,说明以字符processed[i]为中的回文半径等于其对称点的回文半径。
      2. 若对称点的回文半径大于等于max_righti,说明该点的回文半径至少为max_right - i。此时还需要利用中心扩展法进行探测。
    2. i>=max_right意味着需要重新以中心扩展的方式计算回文半径。

Manacher算法该算法示例代码如下:

string preprocess(string s){    string result("$#");    int len = s.length();    for (int i = 0; i < len; ++i) {        result.push_back(s[i]);        result.push_back('#');    }    return result;}string longest_palindromic_substring(string s){    string str = preprocess(s);    int len = str.length();    vector<int> radiuses(len);    int max_right = 0, max_index = 0;    for (int i = 0; i < len; ++i) {        /* core code */        if (max_right > i) {            radiuses[i] = max_right - i < radiuses[2 * max_index - i] ? max_right - i : radiuses[2 * max_index - i];        } else {            radiuses[i] = 1;        }        while (str[i - radiuses[i]] == str[i + radiuses[i]]) {            radiuses[i]++;        }        if (max_right - max_index < radiuses[i]) {            max_index = i;            max_right = i + radiuses[i];        }    }    len = radiuses[max_index] - 1;    int start = max_index - len;    string result;    while (len--) {        result.push_back(str[start + 1]); /* skip '#' */        start += 2;    }    return result;}

一些博客说在每个字符两边插入的特殊字符不能在字符串中出现,其实我个人认为既是出现了也不会影响,关键在于重构回文字符串时如何判断。

参考

  1. O(n)回文子串(Manacher)算法
  2. Manacher算法:求解最长回文字符串,时间复杂度为O(N)
0 0
原创粉丝点击