5.最长回文子串
题目地址: https://leetcode.cn/problems/longest-palindromic-substring/
解法1 - 中心枚举法
思路
有两种回文串
-
长度为偶数:若中心点坐标为i,左右两坐标起点是i - 1, j + 1
-
长度为奇数:若中心点坐标为i,左右两坐标起点是i, j + 1
将字符串中的每个字符视为中心,尝试向两边延伸,最后枚举完所有字符后,结果就是最长字符
枚举到左右两边字符不相等,此时s[l + 1] ~ s[r - 1]就是最长回文串
时间复杂度
假定每个字母都向左右试探n步
0号字母走0步,
1号走1步,
2号走两步
...
n/2走n/2步
...
n-2走2步
n-1走1步
n走0步
\(0+1+2+...+\frac{n}{2} + ... + 2 + 1 + 0\)
根据等差数列公式
$ = \frac{n^2}{4}$
所以是\(N^2\)级别的
代码
//substr的两个参数分别是(起始位置,字符串长度)
class Solution {
public:
string longestPalindrome(string s) {
string ans;
for(int i = 0; i < s.size(); i ++) {
// 枚举奇数长度回文串
int l = i - 1, r = i + 1;
while(l >= 0 && r < s.size() && s[l] == s[r]) --l, ++r;
if(ans.size() < r - l - 1) ans = s.substr(l + 1, r - l - 1);
// 枚举偶数长度回文串
l = i, r = i + 1;
while(l >= 0 && r < s.size() && s[l] == s[r]) --l, ++r;
if(ans.size() < r - l - 1) ans = s.substr(l + 1, r - l - 1);
}
return ans;
}
};
思路2 二分 + Hash
思路
对字符串正序和逆序分别求Hash值,
-
枚举每个字母作为中点,
-
二分测出半径
如果两侧字符串Hash值相等,那就可以继续延长,如果不等,那就缩短
其他需要注意的点
-
当回文串长度为奇数和偶数时,枚举坐标不同,为了方便处理,在每两个字符串之间添加一个#,这样偶数长度回文串也会变成奇数,比如 abba --> a#b#b#a,假设原长度是x,那么添加的#的长度就是x + 1,两者之和等于\(2x + 1\),必然是奇数
-
后面求出回文串长度后,如何确定原字符串长度?
可查看某一端点的字符是#还是字符,如果是#,那就是#比字符多一个,假设半径是mid,总长度就是mid
比如#a#b#a#, mid = 3,字符总数就是3
如果端点值是字符,那就是字符比#多一个,假设半径是mid,总长度就是mid + 1
比如a#b#a, mid = 2,字符总数就是2 + 1 = 3
-
字符串相反, 位置对应问题
0 1 2 3 4 5 6 7 0 7 6 5 4 3 2 1 n = 7, 假设此时i = 4, mid = 2 则应该对比[2,3] 和 [5,6] [2,3]的坐标就是[i-mid, i-1] [5,6]的坐标在反方向是[2,3],即[n-i-(mid-1), n-i]
时间复杂度
枚举n个字母 n,假定每个字母左右都有n个字母,每次耗时\(O(\log^N)\),总的就是\(O(N\log^N)\)级别的
代码
typedef unsigned long long ULL;
class Solution {
public:
static const int N = 2010;
char str[N];
int base = 131;
ULL p[N], hl[N], hr[N];
ULL get(ULL h[], int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
string longestPalindrome(string s) {
strcpy(str + 1, s.c_str());
int n = s.size() * 2;
for(int i = n; i >= 1; i-=2)
{
str[i] = str[i/2];
str[i - 1] = 'z' + 1;
}
p[0] = 1;
for(int i = 1, j = n; i <= n; i++, j--)
{
p[i] = p[i - 1] * base;
hl[i] = hl[i - 1] * base + str[i];
hr[i] = hr[i - 1] * base + str[j];
}
int st = 0, radius = 0;
for(int i = 1; i <= n; i++)
{
int l = 0, r = min(i - 1, n - i);
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(get(hl, i - mid, i - 1) == get(hr, n - i - mid + 1, n - i)) l = mid;
else r = mid - 1;
}
if(l >= radius)
{
st = i;
radius = l;
}
}
string ans;
for(int i = st - radius; i <= st + radius; i++)
if(str[i] <= 'z') ans += str[i];
return ans;
}
};
Q&A
1. 二分必须使用
int mid = (l + r + 1) >> 1;
if(get(hl, i - mid, i - 1) == get(hr, n - i - mid + 1, n - i))
l = mid;
else
r = mid - 1;
不能使用
int mid = (l + r) >> 1;
if(get(hl, i - mid, i - 1) != get(hr, n - i - mid + 1, n - i))
r = mid;
else
l = mid + 1;
以cbabb为例,在加上#字符后变为#c#b#a#b#b,当i = 5
,即字符a时,二分的结果错误
原因是第二种二分,在等号成立时,只能保证这个长度的字符串相等, 而l = mid+1
,扩大了相等的范围,把不相等的字符包括进来了