原题如下:
Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
题目要求很简单,就是求一个字符串的最长回文子串,回文子串的定义为正反相同的连续字符串。
对于类似字符串的子串处理问题,第一反应要想到动态规划思想,这题也是这样。动态规划思想网上到处都是,这里就不再赘述了。当然,运用动态规划之前,需要弄明白题目是否适用动态规划算法,即是否满足最优化原理(原问题的最优解包含子问题的最优解),在划分阶段的时候一定要明确划分是否满足某阶段状态仅由前一阶段状态决定与后一阶段状态无关和子问题重叠性。
对于此题,我们根据回文子串的对称性特点,对问题各阶段进行划分并确定各阶段状态:设P(i,j)表示从第i个字符开始到第j个字符结束的子串,则P(i,j)的最优解即最长回文子串一定包含P(i+1,j-1)的最优解,因而可以将对原问题——P(i,j)的最长回文子串的求解简化为子问题——P(i+1,j-1)最长子串的求解。进而,我们可以确定状态转移方程:若P(i)=P(j),则原问题的解等于子问题的解在首尾加上P(i)和P(j),若P(i)!=P(j),则原问题的解等于两个子问题(分别去掉首尾字符得到子串求解)的解中最优的那个。由此,我们结合递归的思想,得到如下算法代码(包含测试用main函数):
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
string longestPalindrome(string s) {
string Palindrome,Palindrome1,Palindrome2,ss,s1,s2;
ss = s;s1 = s;s2 = s;
int l = s.length();
if(l == 0 || l == 1)return s;
if(s[0] == s[l-1]){
ss.erase(ss.begin());
ss.erase(ss.end()-1);
Palindrome = s[0] + longestPalindrome(ss) + s[l-1];
return Palindrome;
}
else{
s1.erase(s1.end()-1);
s2.erase(s2.begin());
Palindrome1 = longestPalindrome(s1);
Palindrome2 = longestPalindrome(s2);
int l1 = Palindrome1.length();
int l2 = Palindrome2.length();
if(l1>=l2)return Palindrome1;
else return Palindrome2;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
string s = "bb";
if(strcmp(s.c_str(),"") == 0){cout<<"You cannot input a void string"<<endl;return -1;}
string Palindrome = longestPalindrome(s);
cout<<"the Palindrome of s is: "<<Palindrome.c_str()<<endl;
return 0;
}
结果,Leetcode显示超时……看来是递归的算法复杂度(O(n²))太高了,又暂时想不到其他方法,无奈之下只能去网上搜索大神们的答案,结果找到这样一个时间复杂度只有O(n)的算法——Manacher算法,真乃神作,小菜在这里先服了~
算法思想如下(参考http://www.tuicool.com/articles/n6NfIrN):
首先,预处理。为了使得处理方便,在字符串每个字符之间添上“#”,如:“abcd”变为“#a#b#c#d#”,再在首尾添上“$”和“^”,从而使得无论奇数还是偶数回文序列最终都变为奇数序列。
其次,构造P[n]数组,记录每个字符为中心的最长回文子串向左/右的长度(不包括"$"和"^")。如“12212321”,P数组如下:
s: $ # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 # ^
p: 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
最后,显然,只要找到最大的P[i]就能找到最长的回文子串的中心位置。
关于P数组的求法,参考链接中已有详细解释,这里就不再赘述了。
算法代码如下:
#include "stdafx.h"
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string preProcess(string &s){
int n = s.size();
string res;
res.push_back('$');//把$放到字符串头部
res.push_back('#');//以#作为原来字符串中每个字符的间隔
for(int i = 0; i < n; i++)
{
res.push_back(s[i]);
res.push_back('#');
}
res.push_back('^');//以^作为字符串的结尾
return res;
}
string longestPalindrome(string s) {
int l = s.size();
if(l<=1)return s;
//Manacher算法
string str = preProcess(s);
int n = str.size(), id = 0, mx = 0;
vector<int> p(n, 0);
for(int i = 1; i < n-1; i++)
{
p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;//初始化p[i]
while(str[i+p[i]] == str[i-p[i]])p[i]++;
if(i + p[i] > mx)
{
mx = i + p[i];
id = i;
}
}
//遍历p,寻找最大回文长度
int maxLen = 0, index = 0;
for(int i = 1; i < n-1; i++)
if(p[i] > maxLen)
{
maxLen = p[i];
index = i;
}
return s.substr((index - maxLen)/2, maxLen-1);
}
int _tmain(int argc, _TCHAR* argv[])
{
string s = "bb";
if(strcmp(s.c_str(),"") == 0){cout<<"You cannot input a void string"<<endl;return -1;}
string Palindrome = longestPalindrome(s);
cout<<"the Palindrome of s is: "<<Palindrome.c_str()<<endl;
return 0;
}
总结:
1.牢记动态规划的思想和步骤,对于字符串的处理经常会用到;
2.动态规划不一定是最优方法,对于这些情况的特殊解法要多加积累和练习。