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]一定不是回文子串
由此得状态转移方程:
边界: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;
}