题目:
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd" 输出: "bb"
方法1:以每个字符为中心依次搜索最长的回文子串
string longestPalindrom(string s) {
if (s.size() < 2) return s;
int Maxlen = 0, start = 0;
int s_len = s.size();
for (int i = 0; i < s_len;) {
if ((s_len - i) < Maxlen / 2) break;
//如果剩余字符小于最长子串的一半,那么则不可能再出现比此时最长子串更长的回文子串
int right = i, left = i;
while (right < s_len - 1 && s[right] == s[right + 1]) right++;//为偶数个时要让right和left指向不同的字符
i = right + 1;//如果前面是重复的字符则一定以他们为中心进行搜索,那么下一次搜索时应该从重复字符之后的字符进行搜索
while (right < s_len-1 &&left > 0 && s[left-1] == s[right+1]) {//为奇数个时可以直接判断
left--;
right++;
}
if (Maxlen < right - left + 1) {
Maxlen = right - left + 1;
start = left;
}
}
return s.substr(start, Maxlen);
}
方法2:动态规划,用一个二维数组dp[i][j]代表第i个字符到第j个字符是否为回文串。在判断过程中,如果i与j相邻则直接相等即可,如果,i与j不相邻,那么i到j是否为回文串,也与i+1到j-1是否为回文串有关
string longestPalindrom(string s) {
if (s.empty()) return "";
//int dp[s.size()][s.size()] = { 0 };会报错
int **dp = new int*[s.size()];//动态申请变量空间
for (int i = 0; i < s.size(); ++i) {
dp[i] = new int[s.size()];
memset(dp[i], 0, s.size());
}
//dp[i][j]中存储第i个字符到第j个字符是否回文串
int left = 0, right = 0, len = 0;
for (int i = 0; i < s.size(); i++) {
dp[i][i] = 1;
for (int j = 0; j < i; j++) {
dp[j][i] = (s[j] == s[i] && (i - j < 2 || dp[j + 1][i - 1]));
//此时的j到i是否为回文串,要求s[j] == s[i]并且相邻的位置也为回文串,如果ij相邻则不用判断相邻位置
if (dp[j][i]&&len < i - j + 1) {//不仅要判断长度是否还要判断是否为回文串
len = i - j + 1;
left = j;
right = i;
}
}
}
for (int i = 0; i < s.size(); i++)//用完之后一定要释放
delete[]dp[i];
delete[]dp;
return s.substr(left, right - left + 1);
}
方法3:利用经典算法马拉车算法,相对而言技巧性较高。但是直接将算法复杂度由O(n^2)降至O(n)。其主要思路是先将字符串处理成根据普遍性的情况,那就是在每个字符的两边加上#。比如,bob加上#则为#b#o#b#,noon加上#字符则为#n#o#o#n#;这样就不用了再分别考虑回文子串是奇数个还是偶数个。同时,我们观察可以发现此时,回文串的半径正好等于未经处理回文串的长度。因此,在后期计算时,我们只用找经过处理之后的回文子串的半径即可。因为,最后我们要输出,最长的回文子串,所以我们不仅要知道子串的长度,还要知道它的起始位置在何处。这时,在处理好的字符前面加上一个不是字符串s中的也不是#的特殊字符,这样 (中心位置 - 处理后子串初始位置)/2 就是最小回文子串的初始位置。
在遍历寻找最长子串时,主要是利用回文的对称性。如下图所示,其中id代表当前最长子串的中心,i为当前搜索的中心下标,j为与i关于id对称的坐标值为i-2*id,mx为回文串的最右边。数组dp中存储的是以下标位置为中心的最大回文串半径
因此,当i<mx时,dp[i]应该等于mx-i或者是与其对称的j点处的最大半径中较小的一个
如果 当 i>mx时,此时我们就只能以i为中心逐位对比,计算回文串的长度
每次计算完以i为中心的最长子串时,要将id更新为最长子串中心,mx更新为最长子串的最右边
string Manacher(string s) {
//将字符串插入#为了方便处理在其头部插入任意不是#且不是s中的符号
string t = "$#";
for (int i = 0; i < s.size(); i++) {
t += s[i];
t += "#";
}
//用id代表当前最大回文串中心,mx代表回文串的最右边
int id = 0, mx = 0, resLen=0,resCen=0;
vector<int> p(t.size(), 0);//用来存储每个以当前位置为中心的最大回文串长度的半径
for (int i = 1; i < t.size(); i++) {
p[i] = mx > i ? (min(p[2 * id - i], mx - i)) : 1;
//如果mx比i大那么当前以i为中心的最大回文串长度半径为则为p[2 * id - i],mx - i中较小的一个
//利用回文串的对称性,即与前面与i位置对称点的长度一致,或者是mx-i那么长 如果均不是那么假设为1且以i为中心逐个搜索
while ( t[i +p[i]] == t[i - p[i]]) ++p[i];//以i为中心比较两边是否相等计算以i为中心的回文子串长度
//如果最长边界超过mx则更新,id也要更新
if (mx < i + p[i]) {
mx = i + p[i];
id = i;
}
if (resLen < p[i]) {
resLen = p[i];
resCen = i;
}
}
//开始位置为中心位置减去长度/2,到长度位置
return s.substr((resCen - resLen) / 2, resLen-1);
}