题目:
给定一个字符串 s,找到 s 中最长的回文子串
。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
注意
1、子串和序列的区别
2、假设 s 的最大长度为 1000
告诉我们设计一个时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)的算法是可以的(为啥??????????)
3、
从示例中看出,只需输出一个
回文子串
关键点
由于截取字符串有一点的性能消耗,一个等价的方式是:记录最长回文子串的起止下标
和它的长度
,在最后输出的时候再做截取就可了。
一、暴力解法(枚举左右边界)
class Solution:
def longestPalindrome(self, s: str) -> str:
l = len(s)
if l==0:
return ""
l_max = 0
s_max = ""
for i in range(l):#左边界
for j in range(i+1,l+1):#注意右边界从左边界后一个开始,到l+1,因为右边界需取到串的最后一个元素,而(i+1,l)只能取到l-1
s_chlid = s[i:j]
if s_chlid == s_chlid[::-1]:
l_chlid = len(s_chlid)
if l_chlid > l_max:
s_max = s_chlid
l_max = max(l_chlid,l_max)
return s_max
在两个for循环(n^2^)里还有一个遍历(即,判断是否是回文),所以时间复杂度是O(n^3^)
二、动态规划(暴力解法的优化)
1 .关键点
回文串是天然具有状态转移性质的,即,回文串去掉两头后还是回文串(这里回文串的长度要严格大于2)。即,如果一个子串两头若不想等,我们可以直接下结论此子串不是回文串,若两头相等,则用相同的方法继续判断剩下的子串是否是回文。
也就是说,在子串两头相等的情况下
,此子串是否是回文串,取决于
去掉两头后的子串部分是否是回文。
因此,可将状态定义成子串是否是回文:
--状态:dp[i][j],表示子串s[i;j]是否是回文串。//注:这里s是闭区间,包括下表i,j
--得到状态转移方程:dp[i][j] = (s[i] = s[j] and dp[i+1][j-1])//s[i;j]状态为true的条件是(即s[i;j]是回文串的条件):在s两端相等的情况下,其子串dp[i+1][j-1]是回文串
既然是下表访问,就得考虑边界的条件:
--边界条件:(j-1)-(i+1)+1<2,即j-i<3 即j-i+1<4(计算字符串长度的公式),//s[i;j]长度为2或3时不用检查子串是否是回文。
动态规划比暴力解法快就快在了:我们利用状态转移方程快速的得到一个子串是否是回文串,每一步的计算都尽可能的利用了之前计算的结果,这也是非i常典型的空间换时间的思想
--初始化:dp[i][i]==true//单个字符一定是回文串,所以对角线的值可以先赋值为true,由于填表时,并没有用到对角线的值,所以也可不用赋此值。
--输出:在得到一个状态的值为true的时候,记录其实位置和长度,填表完成以后在截取。
2. 示例
题目
:
判断以下字符串是否是回文串
状态方程
:
状态转移方程:dp[i][j] = (s[i] = s[j]) and(j-i<3 or dp[i+1][j-1])//当j-i<3时,子串的长度为2或3,在s[i] = s[j]的条件下,此子串一定是回文串
二维表格
:
动态规划实际上是在填写一张二维表格,由于i<=j(左边界 i 要小于等于右边界 j),所以只用填表格的右上角。
这个表格记录了s所有子串的状态,因为单个字符一定是回文串,所以对角线是true。
由状态方程可知,:dp[i][j]参考dp[i+1][j-1](即表格中它左下方的值),所以填表顺序为:
先升序填列;在升序填行。(沿着行升序的方向,按升序一次填一列,如下图)
注:
由于填表时,并没有用到对角线的值,所以对角线也可不赋值。
动态规划题的思路
:
从一个比较小规模的问题开始,逐步得到较大规模问题的结果,并且在这个过程中记录每一步的结果。
代码
java
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
// 特殊情况判段
if (len < 2){
return s;
}
int maxLen = 1;
int begin = 0;
// 1. 状态定义
// dp[i][j] 表示s[i...j] 是否是回文串
// 2. 初始化
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] chars = s.toCharArray();
// 3. 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
for (int j = 1;j < len;j++){
for (int i = 0; i < j; i++) {
// 头尾字符不相等,不是回文串
if (chars[i] != chars[j]){
dp[i][j] = false;
}else {
// 相等的情况下
// 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串
if (j - i < 3){
dp[i][j] = true;
}else {
// 状态转移
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
// 此时更新记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
// 4. 返回值
return s.substring(begin,begin + maxLen);
}
}
c++
class Solution {
public:
string longestPalindrome(string s) {
int len = s.size();
if(len<2)
{
return s;
}
int maxLen = 1;
int begin = 0;
vector<vector<int>> dp(len,vector<int>(len));//1、创建表
for(int i=0;i<len;i++){
dp[i][i]=1;
}
for(int j=1;j<len;j++){
for(int i=0;i<j;i++){
//2、依据状态转移方程填表
if (s[i]!=s[j]){
dp[i][j]=0;
}
else{
if(j-i<3){
dp[i][j]=1;
}
else{
dp[i][j]=dp[i+1][j-1];
}
}
if(dp[i][j]==1 && j-i+1>maxLen){
maxLen = j-i+1;
begin = i;
}
}
}
return s.substr(begin,maxLen);//s.substr从表索引begin开始,截取长度为maxLen
}
};
--时间复杂度:
O(n2),n为字符串的长度
动态规划的解法依然是枚举左右边界,只不过在两层for循环后,判断子串是否是回文 所用的动态规划 的时间复杂度是O(1)【因为判断s[i:j]是否是回文可由dp[i + 1][j - 1]直接得到,所以时间复杂度是0(1)】,
而在暴力枚举中在两层for循环后,判断子串是否是回文 所用的遍历 的时间复杂度是O(n),所以这道题动态规划是暴力枚举的优化
--空间复杂度:
O(n2)
由上表得,一共n^2^个表格,一个表格代表一个变量,用了一半,即用了n^2^/2个变量,即空间复杂度:O(n^2^)
三、中心扩散法
回文串的枚举可以从两边开始(即方法一,枚举各个子串
的左右边界),也可以从中间位置开始,我们把枚举(各子串)
中心的方法称为“中心扩散法”。
C++
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {
left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size()-1; ++i) {
//回文中心只需枚举到倒数第二位s.size()-2,最后一位不需枚举,因为向右没有字符了,枚举不了了
//回文串的长度可能是奇数,也可能是偶数,所以其中心可能是一个也可能是两个,即,下面的两种情况。
auto [left1, right1] = expandAroundCenter(s, i, i);//长度是奇数,则中心是一个,left1=right1=i
auto [left2, right2] = expandAroundCenter(s, i, i + 1);//长度是偶数,则中心是两个,left2=i ,right2=i+1
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
python
class Solution:
def expandAroundCenter(self, s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return left + 1, right - 1
def longestPalindrome(self, s: str) -> str:
start, end = 0, 0
for i in range(len(s)-1):
left1, right1 = self.expandAroundCenter(s, i, i)
left2, right2 = self.expandAroundCenter(s, i, i + 1)
if right1 - left1 > end - start:
start, end = left1, right1
if right2 - left2 > end - start:
start, end = left2, right2
return s[start: end + 1]
枚举中心位置的个数2(n-1):
因为枚举到倒数第二个,最后一个不枚举,奇数中心和偶数中心情况都是n-1次,所以枚举中心位置的个数是2(n-1)。