5.最长回文子串 - 三种方法
食用指南:
Leetcode专栏开启了,由于博主闭关期末,所以每日只能一题
尽量做到一题多解,先说思路,之后代码实现,会添加必要注释
语法或STL内容会在注意点中点出,新手友好
欢迎关注博主神机百炼专栏,内涵算法基础详细讲解和代码模板
题目描述:
-
给你一个字符串 s,找到 s 中最长的回文子串。
1 <= s.length <= 1000
s 仅由数字和英文字母组成 -
代码背景
class Solution {
public:
string longestPalindrome(string s) {
}
};
- 题目来源:https://leetcode.cn/problems/longest-palindromic-substring/
题目分析:
-
字符串长度才1000,时间复杂度在O(n2)左右都能通过
-
法一:暴力双指针
枚举每个字符作为回文串的中间字符
中间字符分为两种情况:
- 奇数长度的回文串,中间字符无对称元素
- 偶数长度的回文串,中间字符和下一字符对称
假设字符长度为n,则枚举n轮
每轮奇数长度回文串最多枚举n/2种子串
每轮偶数长度回文串也最多枚举n/2种子串
最坏时间复杂度O(n2),106平稳度过 -
法二:动态规划
填表:枚举起点arr[i],枚举终点arr[i][j]
dp填表都是自下而上arr[i][j]子情况有两种:
- s[i] == s[j],则arr[i][j]是否回文需要看上一层arr[i+1][j-1]
但当arr[i][j]所表示的串长度为2时,
arr[1][2] -> arr[2][1],陷入死锁。
但是既然s[i] == s[j],那么i~j必然是回文 - s[i] != s[j],则arr[i][j]必然不是回文串
dp表的基石:所有单个字符都回文,arr[i][i] = 1;
时间复杂度:起点n个,终点n个,填表O(n2),查表找最长O(n2)
可否压缩?不可
找最长子串需要遍历所有arr[i][j],保证arr[i][j] == 1 同时找最大j-i+1 - s[i] == s[j],则arr[i][j]是否回文需要看上一层arr[i+1][j-1]
-
法三:马拉车算法(manacher)
不论回文串长度为奇数/偶数,插空补#号或其他非字符串内符号,
奇数长度则有偶数个空,偶数长度则有奇数个空
则最终连带#得到的回文串都是奇数长字符串哈希O(n)完成打表,之后O(1)判断两串是否相等
枚举每一点作为奇数长回文串中间字,O(n)
二分查找该点为奇数长回文串中间字时回文串的最大长度,最差O(log(n/2))
总时间复杂度:O(2*n + n + nlog(n/2)) = O(n(3+log(n/2)))
最高可以解决串长n = 106
算法模板:
代码实现:
法一:暴力双指针32ms
class Solution {
public:
string longestPalindrome(string s) {
string res;
res += s[0];
int maxx = 0;
int len = s.size();
for(int k = 0; k<len; k++){
int i = k-1, j = k+1;
while(i>=0 && j<len && s[i] == s[j]){
if (j-i+1 > maxx){
maxx = max(maxx, j-i+1);
res = s.substr(i, j-i+1);
}
i--, j++;
}
i = k, j = k+1;
while(i>=0 && j<len && s[i] == s[j]){
if (j-i+1 > maxx){
maxx = max(maxx, j-i+1);
res = s.substr(i, j-i+1);
}
i--, j++;
}
}
return res;
}
};
法二:动态规划636ms
- 数组越界问题很头疼
看了一眼答案发现只要枚举长度,右端点越界时就break
这个写法不用考虑枚举边界,好方法!Get it.
class Solution {
public:
string longestPalindrome(string s) {
int len = s.size();
vector<vector<int>> dp(len, vector<int>(len));
for (int i = 0; i < len; i++) dp[i][i] = 1;
int maxx = 0;
string res;
res += s[0]; //至少单个字符是回文子串
for (int L = 2; L <= len; L++) {
for (int i = 0; i < len; i++) {
int j = L + i - 1;
if (j >= len) break;
if (s[i] != s[j]) dp[i][j] = false;
else if (L == 2) dp[i][j] = true;
else dp[i][j] = dp[i + 1][j - 1];
if (dp[i][j] && L > maxx) {
maxx = L;
res = s.substr(i, L);
}
}
}
return res;
}
};
- int **的二维数组写法:
int ** dp = new int*[len];
for(int i=0; i<len; i++){
dp[i] = new int[len];
}
for(int i=0; i<len; i++){
for(int j=0; j<len; j++){
if (i == j) dp[i][j] = 1;
else dp[i][j] = 0;
}
}
法三:马拉车算法688ms
- manacher算法应用范围非常窄,目前我见过只有最长回文子串可以用
- 但是字符串哈希是完全可以替代KMP的存在,应用范围非常广
class Solution {
public:
typedef unsigned long long ULL;
static const int N = 2e6 + 1;
const int p = 131;
ULL P[N], Hash1[N], Hash2[N];
char ss[N];
ULL get(ULL h[] , int l , int r){
return h[r] - h[l - 1] * P[r-l+1];
}
string longestPalindrome(string s) {
P[0] = 1;
int len = s.size();
int k = 1;
//1,奇偶统一
for(int i=0; i<len; i++){
ss[k++] = s[i];
if (k/2 < len) //奇数位是原串,偶数位是补空,补的空数永远比位数少1
ss[k++] = '#';
}
k--;
//2.打表前缀和 & 打表对称后缀和
for(int i=1,j=k; i<=k; i++,j--){
P[i] = P[i-1]*p;
Hash1[i] = Hash1[i-1]*p + ss[i];
Hash2[i] = Hash2[i-1]*p + ss[j];
}
//3.枚举中点,寻找中点的最长回文
int maxx = 0;
string tmp;
for(int i=1; i<=k; i++){
//以中点为圆心,半径最小0,最大为1~该点前/该点后~k。二分找最大合适半径
int l = 0, r = min(k - i , i - 1);
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(get(Hash1, i - mid, i - 1) == get(Hash2, k + 1 - (i + mid), k + 1 - (i + 1))) l = mid;
else r = mid - 1;
}
if (ss[i-l] == '#'){ //比较时带有=号,因为可能最终结果是单个字母,则半径为0 == 初始maxx
if(maxx <= r){ //#结尾,则最终有半径个字母
maxx = r;
tmp.erase(0);
for(int j=i-r; j<=i+r; j++) tmp+=ss[j];
}
}else{
if (maxx <= r + 1){
maxx = r + 1; //字母结尾,则最终有半径+1个字母
tmp.erase(0);
for(int j=i-r; j<=i+r; j++) tmp+=ss[j];
}
}
}
string res;
for(int i=0; i<tmp.size(); i++)
if(tmp[i] != '#')
res.push_back(tmp[i]);
return res;
}
};
注意点:
- 二维向量写法:
vector<vector<int>> dp(n, vector<int>(n));
//默认初始化为0
-
后缀和数组:
由于前缀和数组已有固定求区间和公式:
距左边远节点 - 距左边近-1节点*P【边长】= 区间和后缀和数组想要直接套用该公式是不行的
因为后缀和数组距离左边近的数值大,远的数值小所以现将后缀和数组表示对称过来,
原本距离右边为1即H[len-1],现表达为距离左边1即H[2]
所以Hash2[i]表达的串是 len+1-i 到 len的子串
于是又满足了i大的距离左边界远,值也大,可以直接套用区间和公式