Leetcode 5: 最长回文子串
5 最长回文子串
找到的题解:
- https://github.com/youngyangyang04/leetcode-master/blob/master/problems/0005.最长回文子串.md
- https://leetcode-cn.com/problems/longest-palindromic-substring/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-bao-gu/
对于给定的字符串,返回其最长的回文子串。难度倒是不高,就是比较麻烦。
朴素暴力写法
最先想到的当然是朴素的暴力写法:从头开始枚举所有的子串,直到找到回文子串
#include <iostream>
using namespace std;
bool judgePalindrome(string s){
for(int i=0;i<s.length()/2;i++){
if(s[i]!=s[s.length()-i-1]){
return false;
}
}
return true;
}
class Solution {
public:
string longestPalindrome(string s) {
int longest_length = 0;
int longest_pos = 0;
for(int i=0;i<s.length();i++){
// 重复尝试所有的开头
// 然后尝试枚举长度,长度最小为当前的最大长度
for(int j=s.length()-i;j>longest_length;j--){
//判断是否为回文子串
bool result = judgePalindrome(s.substr(i,j));
if(result){
longest_pos = i;
longest_length = j;
break;
}
}
}
return s.substr(longest_pos,longest_length);
}
};
int main() {
Solution s;
cout<<s.longestPalindrome("abccb")<<endl;
return 0;
}
复杂度为O(n^3),n为字符串长度。结果不出意外的超时了。
缓存探索回文点写法
另外的一个想法是,既然要判断最长的回文子串,那首先要回文。要回文,首先要相等。因此先跑一遍,用字典记录下所有字符以及其相等的位置。(已知字符仅包括数字和英文字母)
一个想法是,根据所有的相等情况从大到小枚举可能的回文点范围,并记录下回文点的位置,做一次探测。
考虑到回文子串的嵌套特性同一回文点记录回文中心和回文上界,如果长度为偶数则记录0.5。
这样,构建回文相等map的复杂度为O(n),探索所有的相等可能性为O(n^2),叠加上对回文串的检查一样是O(n^3)。只是由于进行了很多记忆,并且只在相等情况下搜索,因此常数项相对来说会小很多。
这个思路的一个基本想法是:对于一个更长的字符串,如果以其中心点的更小长度内曾经探明没有回文子串,那么这个子串一定不回文。
#include <iostream>
#include <map>
#include <vector>
#include <math.h>
using namespace std;
bool judgePalindrome(string s){
for(int i=0;i<s.length()/2;i++){
if(s[i]!=s[s.length()-i-1]){
return false;
}
}
return true;
}
class Solution {
public:
string longestPalindrome(string s) {
int longest_length = 0;
int longest_pos = 0;
map<char,vector<int>> store;
map<double,int> record; // record[i]=j表示在回文点i上探索过的最大长度为j
for(int i=0;i<s.length();i++){
store[s[i]].push_back(i);
}
for ( const auto &myPair : store ) {
// 遍历所有的key
// cout<<myPair.first<<endl;
auto arr = myPair.second;
if(arr.size()<=1) {
continue;
}
// for(int i=0;i<arr.size();i++){
// cout<<arr[i]<<" ";
// }
// cout<<endl;
for(int i=0;i<arr.size();i++){
for(int j=i+1;j<arr.size();j++){
int pos1 = arr[i];
int pos2 = arr[j];
double mid_point = (double)(pos1+pos2)/2;
int cur_length = ceil(pos2-mid_point);
// 如果在record中有,就不进行记录
bool isRecord = false;
if(record.find(mid_point)!=record.end()){
if(record[mid_point]<=cur_length){
isRecord = true;
}
}
if(isRecord) {
continue;
}
else if(pos2-pos1+1<longest_length){
continue;
}
// 无记录,则进行搜索
bool result = judgePalindrome(s.substr(pos1,pos2-pos1+1));
if(result){
longest_length = pos2-pos1+1;
longest_pos = pos1;
}else{
// 如果小的字符串内部没有回文,那么更大的长度也不会有
record[mid_point] = cur_length;
}
}
}
}
if(longest_length==0){
longest_length = 1;
longest_pos = 0;
}
return s.substr(longest_pos,longest_length);
}
};
结果挺一般的,也就属于做出来的级别
执行用时:708 ms, 在所有 C++ 提交中击败了8.91%的用户
内存消耗:322.3 MB, 在所有 C++ 提交中击败了28.12%的用户
通过测试用例:180 / 180
动态规划做法
此外,由于回文子串的可扩展性,是具备最优子结构的。因此这道题可以考虑用动态规划来做
- 确定dp数组的大小,以及其内容,代表的含义
- 写dp的递推公式
- dp的初始值是多少
- dp应该如何开始遍历,或者采用记忆化搜索的形式?
- 举个例子推导dp数组
按照这个思路:
-
确定dp数组:dp[i,j]表示字符串[i,j]是否为回文子字符串,其内容为true/false
-
递推公式(默认i≤j)
- i==j时,dp[i,j]=true
- s[i]≠s[j]时,false
- s[i]=s[j]时,如果其子区间[i+1,j-1]同样回文,则可以递推,反之则不行。也即当dp[i+1,j-1]为true时可以递推
-
初始值为false,全部为匹配
-
确定执行顺序(注意边界):第一个维度需要依赖其后的信息,第二个维度需要依赖前一个的信息。如果横轴为第一个维度,纵轴为第二个维度。
- 当前为九宫格的中心,则计算出当前结果需要从右下角算出。即遍历顺序为从最右下角向上计算。从下到上,从右到左。因此第一个维度。
- 执行范围为九宫格的左上三角形。这里可以设置,所有不在当前dp维度内的均为true
-
尝试一下手填abb
- 正确
#include <iostream> #include <vector> using namespace std; class Solution { public: string longestPalindrome(string s) { vector<vector<bool>> dp(s.length(),vector<bool>(s.length(),false)); int longest_pos = 0; int longest_length = 1; for(int j=0;j<s.length();j++){ for(int i=j;i>=0;i--){ if(i==j){ dp[i][j]=true; } else if((s[i]==s[j] && dp[i+1][j-1]) || (s[i]==s[j] && i+1 > j-1 )){ dp[i][j]=true; } // if(dp[i][j]){ // cout<<i<<" "<<j<<" "<<s.substr(i,j-i+1)<<endl; // } if(dp[i][j] && j-i+1>longest_length){ longest_length = j-i+1; longest_pos = i; } } } return s.substr(longest_pos,longest_length); } }; int main() { Solution s; cout<<s.longestPalindrome("cbbd")<<endl; return 0; }
执行用时:428 ms, 在所有 C++ 提交中击败了**34.57%**的用户
内存消耗:29.4 MB, 在所有 C++ 提交中击败了**48.89%**的用户
通过测试用例:180 / 180
双指针方法
该方法的想法是尝试扩展。回文字符串一定是从某个中心出发的,这个中心可能是一个字符或者两个字符,因此总共应该有n+n-1个中心。
class Solution { public: int left = 0; int right = 0; int maxLength = 0; string longestPalindrome(string s) { int result = 0; for (int i = 0; i < s.size(); i++) { extend(s, i, i, s.size()); // 以i为中心 extend(s, i, i + 1, s.size()); // 以i和i+1为中心 } return s.substr(left, maxLength); } void extend(const string& s, int i, int j, int n) { while (i >= 0 && j < n && s[i] == s[j]) { if (j - i + 1 > maxLength) { left = i; right = j; maxLength = j - i + 1; } i--; j++; } } };
执行用时:16 ms, 在所有 C++ 提交中击败了**91.46%**的用户
内存消耗:7.1 MB, 在所有 C++ 提交中击败了**79.68%**的用户
通过测试用例:180 / 180
一下子快了很多
马拉车算法
另外有一个专门的算法****Manacher's Algorithm****
该算法可以解决最长回文子字符串问题,时间复杂度为线性
```cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
string preprocess(string s){
int n=s.length();
if(n==0){
return "^$";
}
string ret = "^";
for(int i=0;i<n;i++){
ret = ret + "#" + s[i];
}
ret += "#$";
return ret;
}
class Solution {
public:
string longestPalindrome(string s) {
string T = preprocess(s);
int n = T.length();
vector<int> P(n,0);
int C=0,R=0;
for(int i=1;i<n-1;i++){
int i_mirror=2*C-i;
if(R>i){
P[i] = min(R-i,P[i_mirror]);
}else{
P[i] = 0;
}
while(T[i+1+P[i]] == T[i-1-P[i]]){
P[i] ++;
}
if(i+P[i]>R){
C=i;
R=i+P[i];
}
}
int maxLen = 0;
int centerIndex = 0;
for(int i=1;i<n-1;i++){
if(P[i] > maxLen){
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;
return s.substr(start,maxLen);
}
};
int main() {
Solution s;
cout<<s.longestPalindrome("cbbd")<<endl;
return 0;
}
```
执行用时:**84 ms**, 在所有 C++ 提交中击败了**62.05%**的用户
内存消耗:**312.5 MB**, 在所有 C++ 提交中击败了**28.35%**的用户
通过测试用例:**180 / 180**
但是这个算法实在是有点复杂