最长回文子串 (longest-palindromic-substring)
在刷leetcode的动态规划部分的时候,遇到的第一个题目是5.最长回文子串 (longest-palindromic-substring) ,这道题不用动态规划去做会更简单一些,这里提供这样一种解题方法和代码(vs版和leetcode版,实测可运行)
解题思路
我们这里使用一个字符串"abbc"作为输入的样例,这样看起来会更加易懂
1. 扩展字符串:
将原字符串的每一个字符用一个原字符串中没有出现过的字符包围(这里使用’#'字符)
实例:“a b b c"
→
\rightarrow
→ "# a # b # b # c #” (这里的空格仅为了展示清晰,实际运行过程中不存在)
2. 计算最长回文串:
对每一个字符,求以该字符为单核的最长子串长度。
实例:以加粗字符为核
字符串 | 最长回文子串 | 最长回文子串长度 |
---|---|---|
# a # b # b # c # | # | 1 |
# a # b # b # c # | #a# | 3 |
# a # b # b # c # | # | 1 |
# a # b # b # c # | #b# | 3 |
# a # b # b # c # | #b#b# | 5 |
# a # b # b # c # | #b# | 3 |
# a # b # b # c # | # | 1 |
# a # b # b # c # | #c# | 3 |
# a # b # b # c # | # | 1 |
3. 长度换算:
我们可以看到,对于第二步中的每一个最长回文子串,去掉字符‘#’ 之后的字符串就是一个回文串,而最长的那一个字符串删去字符‘#’ 之后就是原字符串的最长回文子串。我们可以看作每一个原字符串字符与一个井字符 一起出现,同时最后多一个井字符结尾,所以
最长回文子串长度 maxLength = (maxLength’(带有‘#’的最长回文子串长度) - 1) / 2
实际运行时,因为每一个带’#'的回文子串的长度很显然都是奇数,所以只需要简单的除以2就可以。
4. 回文子串核的索引:
我们现在有扩展后的字符串的回文子串的核的位置,我们称为coreIdx’, 然后我们要求原字符串中回文子串的核的位置,我们称为coreIdx, 这里我们假设双核的子串(如"abba")的核的索引为双核中左侧的索引,也就是第一个字符b的索引。
首先,我们发现
- 所有‘#’的索引都是偶数(这是很显然的)
- 所有‘#’为核的回文子串去掉‘#’后都是双核(因为两侧的原字符串字符成对出现)
- 所有以原字符串为核的回文子串去掉’#'后都是单核(原因同2)
那么,我们对于所有以‘#’为核的字符串来说,它在原字符串中核的位置应该等于该‘#’左侧字符在原字符串中的索引。
分情况讨论:
- 以原字符串字符为核, coreIdx = (coreIdx’ - 1) / 2
- 以’#‘为核, coreIdx = ((coreIdx’ - 1) - 1) / 2 = coreIdx’ / 2 - 1, 从上面的结论1中,coreIdx是偶数,所以实际上这个表达式和 (coreIdx’ - 1) / 2是等价的
也就是说,我们可以用coreIdx = (coreIdx’ - 1) / 2去求所有的回文子串的核的位置,有了核的位置以及子串长度(子串长度可以表示单核还是双核),求出来实际的子串就非常简单了。
时间复杂度
一个独立的循环,运行2n+1次,每个循环最多运行n次,最坏情况下每次循环约执行n/2次,时间复杂度
O(
n
2
n^2
n2)
代码
visual studio 2019 版
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <vector>
using namespace std;
void leetcode_dp_5_s1() {
string s;
cin >> s; // 输入字符串
int l = s.size(); // l: 原字符串长度
string b = s; // b: backup,原字符串备份
for (int i = 0; i <= l; i++) {
s.insert(2 * i, 1, '#'); // 字符串扩展
}
l = s.size(); // l: 扩展后字符串长度
vector<int> strLen; // 记录扩展后每一个字符的回文子串长度(这里实际记录的是核的单侧有多少字符)
for (int i = 0; i <= l; i++) strLen.push_back(0);
int j = 0;
while (j < l) {
int curStep = 1;
// 对于每一个字符来说,如果从核的索引 +- curStep 没有越界 且 两侧字符相等,那么回文子串长度+1
while (!(j - curStep < 0 || j + curStep >= l) && s[j - curStep] == s[j + curStep]) {
strLen[j]++;
curStep++;
}
j++;
}
int max = *max_element(strLen.begin(), strLen.end()); // 求出最大值
int idx = distance(strLen.begin(), max_element(strLen.begin(), strLen.end())); // 求出最大值索引
int gap = (max - 1) / 2; // 计算原字符串中核的左侧有多少元素
int left = (idx - 1) / 2 - gap; // 计算核的位置
string res = b.substr(left, max); // 获取子串
cout << res;
}
leetcode 版
class Solution {
public:
string longestPalindrome(string s) {
int l = s.size();
string b = s;
for (int i = 0; i <= l; i++) {
s.insert(2 * i, 1, '#');
}
l = s.size();
vector<int> strLen;
for (int i = 0; i <= l; i++) strLen.push_back(0);
int j = 0;
while (j < l) {
int curStep = 1;
while (!(j - curStep < 0 || j + curStep >= l) && s[j - curStep] == s[j + curStep]) {
strLen[j]++;
curStep++;
}
j++;
}
int max = *max_element(strLen.begin(), strLen.end());
int idx = distance(strLen.begin(), max_element(strLen.begin(), strLen.end()));
int gap = (max - 1) / 2;
int left = (idx - 1) / 2 - gap;
string res = b.substr(left, max);
return res;
}
};