最长回文子串(动态规划,字符串hash+二分)——附完整实现代码

1 问题描述

给出一个字符串,求字符串的最长回文子串的长度

样例:
字符串“ PATZJUJZTACCBCC”的最长回文子串为"ATZJUJZTA"长度为9.

2 求解

暴力方法:枚举子串两个端点i和j,判断在[i,j]区间内的子串是否回文。从时间复杂来看,枚举端点需要O(n2),判断回文需要O(n),因此总复杂度为O(n3)

有人可能会想到用最长公共子序列(LCS)来求解,把字符串S倒过来变成字符串T,求S和T的LCS。但实际上这种方法是错误的,因为一旦S中存在它的子串和它的倒序,就会出错。

如字符串S=“ABCDZJUDCBA”,将其倒过来T=“ABCDUJZDCBA”,这样得到的最长公共子串为“ABCD”,长度为4,实际上最长回文子串长度为1。

2.1 动态规划(O(n2))

使用动态规划可以将时间复杂度降低到O(n2)
dp[i][j]表示S[i]至S[j]所表示的子串是否是回文串,是则为1,不是为0,根据S[i]是否等于S[j],可以把转移情况分为两类,

  • 1 若S[i]==S[j],那么只要S[i+1]至S[j-1]是回文串,S[i]至S[j]就是回文子串;如果S[i+1]至S[j-1]不是回文子串,那么S[i]至S[j]也不是回文子串;
  • 2 若S[i] != S[j], 那么S[i]至S[j]一定不是回文子串
    由此得状态转移方程:
    d p [ i ] [ j ] = { d p [ i + 1 ] [ j 1 ] , S [ i ] = S [ i ] 0 , S [ i ] ! = S [ i ] dp[i][j]=\left\{ \begin{aligned} dp[i+1][j-1],S[i] = S[i] \\ 0,S[i] != S[i] \\ \end{aligned} \right.

边界:dp[i][i] = 0, dp[i][i+1] = (S[i] == S[i+1])? 1 : 0

如果按照i和j从小到大枚举子串的两个端点,然后更新dp,会无法保证dp[i+1][j-1]已经被计算过,从而无法得到正确的dp[i][j].

例如,先固定i为0,然后枚举j从2开始。当求解dp[0][2]时,会转换为dp[1][1],而dp[1][1]是在初始化时候得到的。求解dp[0][3]时,会转换为dp[1][2],而dp[1][2]是在初始化时候得到的。求解dp[0][4]时,会转换为dp[1][3],而dp[1][3]并不是已经计算过的值,因此无法状态转移。

解决办法:根据递推从边界出发的原理,注意到边界表示长度为1和2的子串,且每次转移都对子串的长度减1,因此可以考虑按子串的长度和子串的初始位置进行枚举,即第一次将子串长度为3的dp全部求出,第二次将子串长度为4的dp全部求出……这样就可以避免状态无法转移的问题。

2.2 字符串hash + 二分(O(nlogn))

对于一个给定的字符串str,可以先求其字符串hash数组H1,然后将str反转,求出反转字符串str的hash数组H2,接着分回文串(不是原字符串)的奇偶情况讨论:

  • 1 回文串的长度是奇数:枚举回文中心i,二分子串的半径k,找到最大使子串[i-k,i+k]是回文串的k。
    • 其中判断子串[i-k,i+k]是回文串等价于判断str的两个子串[i-k,i]与[i,i+k]是否是相反的串;
    • 而这等价于判断str[i-k,i]子串与反转字符串rstr的[len - 1-(i+k),len - 1 - i]子串是否是相同的([a,b]在反转字符串中的位置为[len - 1 -b, len -1 - a]),因此只需要判断H1[i-k…i]与H2[i-(i+k)…len-1-i]是否是相等的即可。
  • 2 回文串的长度是偶数:枚举回文空隙点,令i表示空隙点左边第一个元素的下标,二分子串的半径l,找到最大使子串[i-k+1,i+k]是回文串的k。
    • 其中判断子串[i-k+1,i+k]是回文串等价于判断str的两个子串[i-k+1,i]与[i+1,i+k]是否是相反的串;
    • 而这等价与判断str的[i-k+1,i]子串与反字符串rstr的[len - 1 - (i+k),len - 1 - (i + 1)]子串是否是相同的,因此只需要判断H1[i-k+1…i]与H2[len - 1- (i+k)…len - 1- (i+1)]是否相等即可

3 实现代码

3.1 动态规划

#include <cstdio>
#include <cstring>

const int MAXN =1010;
char S[MAXN];
int dp[MAXN][MAXN];

int main(int argc, char const *argv[])
{
    fgets(S, MAXN, stdin);
    int len = strlen(S) - 1;
    int ans = 1;
    memset(dp, 0, sizeof(dp));

    //边界
    for (int i = 0; i < len; ++i)
    {
        dp[i][i] = 1;
        if(i < len - 1){
            if(S[i] == S[i + 1]){
                dp[i][i+1] = 1;
                ans = 2;
            }
        }
    }

    //状态转移方程
    for (int L = 3; L <= len; ++L)//枚举字符串长度
    {
        for (int i = 0; i + L - 1 < len; ++i)//枚举字符串的起始端点
        {
           int j = i + L - 1;//字符串的右端点
           if(S[i] == S[j] && dp[i + 1][j - 1] == 1){
            dp[i][j] = 1;
            ans = L;//更新最长回文串长度
           }
        }
    }
    printf("%d\n", ans);
    return 0;
}
/*
PATZJUJZTACCBCC
ABCDZJUDCBA
*/

3.2 字符串hash + 二分

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using std::cin; using std::cout;
using std::endl; using std::vector;
using std::reverse; using std::max;
using std::string; using std::min;

typedef long long LL;
const int P = 10000019;//计算hash值的进制数
const int MOD = 1000000007;//计算hash值的模数
const int MAXN = 200010;//字符串最大长度
LL PowP[MAXN], H1[MAXN], H2[MAXN];//PowP[i]存放p^i%MOD,H1、H2分别存放str1和str2的hash

void Init(int len){//初始化PowP
    PowP[0] = 1;
    for (int i = 1; i <= len; ++i)
    {
        PowP[i] = (PowP[i -1] * P) % MOD;
    }
}

//计算字符串str的hash
void calH(LL H[], string &str){
    H[0] = str[0];
    for (int i = 1; i < str.length(); ++i)
    {
        H[i] = (H[i - 1] * P + str[i]) % MOD;
    }
}

//计算H[i...j]
int calSingleSubH(LL H[], int i , int j){
    if(i == 0) return H[j];
    else return ((H[j] - H[i-1] * PowP[j - i + 1])% MOD + MOD) % MOD;
}

//在[l,r]里二分回文半径;len:字符串长;i:对称点;
//isEven:求奇回文时为0,偶回文为1;
//寻找最后一个满足"hashL==hashR"的回文半径
//等价于寻找第一个满足条件的"hashL != hashR"的回文半径,减1
int binartSearch(int l, int r, int len, int i, int isEven){
    while(l < r){
        int mid = (r + l) / 2;
        int H1L = i - mid + isEven, H1R = i;
        int H2L = len - 1 - (i + mid), H2R = len - 1 - (i + isEven);
        int hashL = calSingleSubH(H1, H1L, H1R);
        int hashR = calSingleSubH(H2, H2L, H2R);

        if(hashL != hashR){
            r = mid;
        }else{
            l = mid + 1;
        }
    }

    return l - 1;
}


int main(int argc, char const *argv[])
{
    string str;
    getline(cin, str);

    Init(str.length());

    calH(H1, str);

    reverse(str.begin(), str.end());
    calH(H2, str);

    int ans = 0;
    //奇回文
    for (int i = 0; i < str.length(); ++i)
    {
        //二分上界为分界点i的左右长度较小值加1
        //回文半径的右边界,防止回文半径长度超过字符串长度
        int maxLen = min(i, (int)str.length() - 1 - i) + 1;
        int k = binartSearch(0, maxLen, str.length(), i, 0);
        ans = max(ans, k * 2 + 1);
    }
    //偶回文
    for (int i = 0; i < str.length(); ++i)
    {
        //二分上界为分界点i的左右长度较小值加1(注意:左长为i+1)
        int maxLen = min(i + 1, (int)str.length() - 1 - i) + 1;
        int k = binartSearch(0, maxLen, str.length(), i, 1);
        ans = max(ans, k * 2);
    }

    printf("%d\n", ans);
    return 0;
}
发布了378 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_33375598/article/details/104454722