LeetCode:5. Longest Palindromic Substring
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Example 2:
Input: "cbbd"
Output: "bb"
题目的意思就是求一个字符串的最大连续回文子串。
思路一:滑动窗口
其实这个滑动窗口的方法也是在暴力求解法的基础上的一个改进,由于暴力求解法时间通不过所以这里就不写出来了。
思路是这样的,假设某个时刻最长回文字串长度为 maxLen,对于位置i(i=0,1,…,n),设一个窗口左侧为i,右侧为j(j=i,i+1,…,n),窗口大小为 maxLen。判断这个子串是否是回文。如果是,则更新最大长度的保存值maxLen。这样就跳过了比maxLen小的字串,每次都保证查找的子串长度大于等于 maxLen。
class Solution:
def isPalindromic(self,s: str) -> bool:
sLen = len(s)
if sLen == 1:
return True
mid = int((sLen)/2)
i = 0
for i in range(0,mid):
if s[i] != s[sLen-i-1]:
return False
return True
def longestPalindrome(self, s: str) -> str:
ans=""
sLen = len(s)
if sLen == 1:
return s
maxLen = 0
i = 0
j = 0
k = 0
for i in range(sLen):
left = i
right = i+ maxLen
right = right+1 if left == right else right
while right <= sLen:
if self.isPalindromic(s[left:right]) and len(s[left:right]) >= maxLen:
ans=s[left:right]
maxLen = len(s[left:right])
right+=1
return ans
复杂度分析
最里面判断是否是回文的时间是 O ( n 2 ) O(\frac{n}{2}) O(2n)。外面双层循环是 O ( n 2 ) O(n^2) O(n2),所以其实这个滑动窗口的方法时间复杂度还是 O ( n 3 ) O(n^3) O(n3)。不过系统还是可以AC的。
思路二:动态规划
举个例子,有字符串 “ababa” ,如果我们已经知道 “bab” 是一个回文子串,很显然,只有当"bab"的左右两侧字符串相同时 , "ababa"才是回文。
我们定义函数 P(i,j)如下:
P ( i , j ) = { t r u e , S i , S i + 1 , . . . , S j ( p a l i n d r o m e ) f a l s e , o t h e r w i s e P(i,j)= \begin{cases} true, & S_i,S_{i+1},...,S_j\:(palindrome)\\ false, & otherwise \end{cases} P(i,j)={true,false,Si,Si+1,...,Sj(palindrome)otherwise
也就是说当 S i , S i + 1 , . . . , S j S_i,S_{i+1},...,S_j Si,Si+1,...,Sj 这个字符串是回文的时候 P(i,j) 为true,否为false。因此 P ( i , j ) = ( ( P ( i + 1 , j − 1 ) = = t r u e ) a n d ( S i = = S j ) ) P(i,j)=((P(i+1,j-1)==true)and(S_i==S_j)) P(i,j)=((P(i+1,j−1)==true)and(Si==Sj))。此外要注意的最基本的两个点:
P ( i , i ) = t r u e P(i,i)=true P(i,i)=true
P ( i , i + 1 ) = ( S i = = S i + 1 ) P(i,i+1)=(S_i==S_{i+1}) P(i,i+1)=(Si==Si+1)
"""
dynamic programming
规则,如果P(i,j)是回文字符串,那么S[i]=S[j],且P(i+1, j-1)也是回文串。
一个变量保存目前位置之前的最长回文子串 maxl
我们用一个boolean dp[l][r]表示字符串从i到j这段是否为回文。
试想如果dp[l][r]=true,我们要判断dp[l-1][r+1]是否为回文,只需要判断字符串在(l-1)和(r+1)两个位置是否为相同的字符,是不是减少了很多重复计算。
进入正题,动态规划关键是找到初始状态和状态转移方程。
初始状态,l=r时,此时dp[l][r]=true。
状态转移方程,dp[l][r]=true并且(l-1)和(r+1)两个位置为相同的字符,此时dp[l-1][r+1]=true
"""
class Solution:
def longestPalindrome(self, s: str) -> str:
st = 0
maxl = 0
sLen = len(s)
dp = [[0 for i in range(sLen)] for j in range(sLen)]
for i in reversed(range(len(s))):
for j in range(i,len(s)):
if s[i] == s[j] and (i+1>j-1 or dp[i+1][j-1] ):
dp[i][j] = 1
if j-i+1 > maxl:
st = i
maxl = j-i+1
return s[st:st+maxl]
这里值得注意的是,i要从后向前遍历。然后剩下的就是按照上面的公式写出代码即可。
里面的i+1>j-1
这个条件,其实就是防止左右边界溢出(i=sLen)。
时间复杂度
动态规划的方法时间复杂度显示为 O ( n 2 ) O(n^2) O(n2)
思路三:Expand Around Center
官方的解答中有个叫 Expand Around Center
的方法。主要思想就是回文字符串总是中心对称的。一个长为n的字符串有2n-1个字串,只要判断这些子串是不是中心对称即可。当然实现的方法也很巧妙,注意下面的两个len:len1和len2,前一个是以当前这一个字符向左右展开(Expand),判断左右字符串是否对称(调用expandAroundCenter方法),后一个是以相邻两个字符串向左右展开,判断左右字符串是否对称,然后比较两个长度大小,取较长的。最后根据这个长度,以及当前字符位置,得到左右边界,从而得到一个回文子串。
# Expand Around Center
class Solution:
def expandAroundCenter(self,s: str, left: int, right: int)-> int:
l = left
r = right
while(l >= 0 and r < len(s) and s[l] == s[r]):
l-=1
r+=1
return r-l-1
def longestPalindrome(self, s: str) -> str:
if (s is None or len(s) < 1):
return ""
start = 0
end = 0
for i in range(len(s)):
len1 = self.expandAroundCenter(s, i, i)
len2 = self.expandAroundCenter(s, i, i + 1)
lenMax = max(len1, len2)
if (lenMax > end - start):
start = i - int((lenMax - 1) / 2)
end = i + int(lenMax / 2)
print(start,end)
return s[start:end+1]
复杂度
上面动态规划的方法用到了一个二维数组,所以空间复杂度为
O
(
n
2
)
O(n^2)
O(n2),而这个 Expand Around Center
的方法的空间复杂度只有O(1)。两者的时间复杂度都为
O
(
n
2
)
O(n^2)
O(n2)
THE END.