Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1:
1 | Input: "babad" |
Example 2:
1 | Input: "cbbd" |
这道题解法比较多,所以特意拿来记录一下,我自己的解法应该是一种从中间到两头的算法。
第一种解法是将字符串 S 翻转为 S’ 后看二者的最长的共同子串。这种解法有个问题,就是如果有有个非回文串正好是共同子串结果就不对。Solution 中给出一个例子:S = “abacdfgdcaba”,S′ = “abacdgfdcaba”,结果显然不对。所以我们需要检查公共子串是否是回文串。
第二种解法就是 Brute Force 了,其实也是最简单的方法,O(N) 判断是否为回文串,O(N^2) 生成所有的字符串,提交目测是会超时的。
第三种解法是动态规划法,这种算法非常有意思,经常让人感觉特别的 smart,我们来看看是怎么处理的。
Solution 中定义 Pij = true if substring Si, Sj is a palindrome, otherwise false:
Base case:
到这里应该没啥难理解的,我们来写写代码:
1 | def longestPalindrome(s: str) -> str: |
网上有很多个 DP 的版本,但我觉得这个版本是最容易理解的,它参考自这里,其余的版本也都类似,最精简的是下面这个版本,它同样出自刚刚的参考链接。
1 | def longestPalindrome(s): |
事实上,我们知道 j-i 一定是大于等于 2 的,如果 j-i < 2 也就是我们 Base 的两种情况:
j-i=1 and s[i] == s[j]
,此时dp[i][j] = 1
等价于dp[i][i+1] = 1
j-i=0 and s[i] == s[j]
,此时dp[i][j] = 1
等价于dp[i][i] = 1
所以这种简化实质上并没有减少多少操作,而且看起来不甚直观(当然理解了会好些)。在此基础上还可以进一步简化,也就是用 len(ans)
直接代替 max_length
,这个就不那么重要了,略过。
第四种解法就是从中间向两端延伸,我先把自己的版本讲解一下,然后我们简单地提一下如何优化。
当时的思路是这样的:
- 从第一个元素开始向两端延伸,直到超出边界或两端元素不相等为止;
- 然后下一个元素直到所有元素执行一遍,每次延伸完算一下所获得回文串的长度,如果比之前长则更新;
- 在向两端延伸时,只有一个需要特别注意的地方,就是我们按偶数个或奇数个个数延伸
- 如果是奇数个,则不考虑当前元素
- 如果是偶数个,则考虑当前元素
代码如下:
1 | def longestPalindrome(s): |
我觉得这种解法是除 Brute Force 外最容易理解的,非常的直观明了。如果想要对其进行优化,只需把 while 和 psx 这一段抽象成一个函数即可,如下所示:
1 | def longestPalindrome(s): |
这个解法也非常容易理解,而且看起来更加清晰,它参考自这里。
第四种解法的时间复杂度和 DP 一样,也是 O(N^2),空间复杂度为 O(1)。还有其他类似的一些解法,比如这个,但其本质和咱们上面提到的没什么区别,就此略过。
最后我们看下第五种算法,也是唯一一种 O(N) 时间复杂度的算法。这个算法名字叫 Manacher’s Algorithm,我一开始看了这个:Programming Problems and Competitions :: HackerRank,结果到后面没看懂,后来又发现了这个视频:Longest Palindromic Substring O(N) Manacher’s Algorithm - YouTube,看了两遍才基本看懂,感觉很神奇,下面的代码也是仿照视频中的代码写的。其实写代码不难,难在对算法本身的理解。
接下来我简单地说一下该算法的基本思想以及其中的一些小 trick。
每隔一个字符添加一个标记,比如 “#”,防止连续出现的重复字符对算法造成干扰(因为两个连续重复的字符都是其中心)
每一个字符作为中心向两边延伸(Expanding for every center)
回文串是中心对称的,这意味着如果知道了中心左边字符对应的长度,右边的长度可以直接通过对称性得到。比如:ababa,前 3 个字符对应的回文串长度分别是 1 3 5,那后两个自然就是 3 和 1。
接下来有两种不同的越界情况,即回文串左边或右边还有字符(还是以上一条的例子说明)。
- 当右边(也就是后两个字符)有字符时,它们 center 的回文串长度一定大于等于对称的左边。比如 ababab,后三个字符实际上是 5 3 1。所以,就从复制过来的对称左边的长度开始 expanding,比如倒数第 3 个,复制过来的 center 长度是 3,那么就从 3 开始往外 expanding,也就意味着 aba 是不需要执行的,只要向外 expanding 1 个字符即可。
- 当左边(也就是最前面)有字符时,同样从原来的长度开始扩展。比如 bababa,第 3 个字符作为 center 时长度本来是 3,现在向外 expanding 1 个字符,变为 5。原来对称的右边此时不能直接复制新得到的长度,比如第 2 个字符作为 center 现在的长度是 3,不能直接复制到原来对称的右边(即最后一个字符),因为最后一个字符作为 center 的长度明显是 1。也就是说,越界时,对称性不一定成立。此时,对称的右边字符作为 center 时的长度变为:
右边界-字符的 index
,比如倒数第 2 个字符,长度并不是原来对称的 5,而是13-10
(@b#a#b#a#b#a$
)。
整理之后就是(截图来自那个视频):
具体代码如下:
1 | def longestPalindrome(s): |
可以看出该算法的核心就是利用回文串的对称性减少计算。这里也有一个类似的解法:Manacher algorithm in Python O(n) - LeetCode Discuss),其实基本是一样的,毕竟思路就是那样的。另外,这里:Taro Kuriyama | Visualization 有也有一个解法,可以参考一下。