📅  最后修改于: 2023-12-03 14:55:36.334000             🧑  作者: Mango
回文是指正着和反着读都一样的字符串。本文介绍如何查找通过从字符串中删除或改组字符形成的最长回文。
给定一个字符串,查找通过从字符串中删除或改组字符形成的最长回文。例如,对于字符串 abcbda
,可以删除字符 a
和 d
,来形成回文 bcb
。
最简单直接的方法是枚举所有可能的子串,判断其是否为回文。对于长度为 $n$ 的字符串,有 $O(n^3)$ 种子串,每个子串判断是否回文需要 $O(n)$ 的时间,所以总时间复杂度为 $O(n^4)$。代码如下:
def is_palindrome(s):
return s == s[::-1]
def longest_palindrome(s):
n = len(s)
longest = ''
for i in range(n):
for j in range(i, n):
if is_palindrome(s[i:j+1]):
if len(longest) < len(s[i:j+1]):
longest = s[i:j+1]
return longest
可以用动态规划算法优化暴力法。定义 $dp[i][j]$ 表示字符串从 $i$ 到 $j$ 是否为回文,有以下状态转移方程:
$$ dp[i][j] = \begin{cases} 1, & i = j \ s[i] == s[j], & j = i + 1 \ s[i] == s[j] \land dp[i+1][j-1], & j > i + 1 \end{cases} $$
这个方程的意思是,只有单个字符时一定是回文,两个字符如果相等也是回文,长度大于等于 $3$ 的字符串,当两端字符相等且去掉两端后的子串也是回文时才是回文。初始化时, $dp[i][i] = 1$。
我们需要记录最长回文的长度和起始位置,代码如下:
def longest_palindrome(s):
n = len(s)
dp = [[0] * n for _ in range(n)]
longest_len = 0
longest_start = 0
for j in range(n):
for i in range(0, j+1):
if s[i] == s[j] and (j - i < 2 or dp[i+1][j-1]):
dp[i][j] = 1
if j - i + 1 > longest_len:
longest_len = j - i + 1
longest_start = i
return s[longest_start:longest_start+longest_len]
这个算法的时间复杂度为 $O(n^2)$,因为只需要求 $dp[i][j]$ 一次。
上面两种算法都不能返回删除或改组后的最长回文,只能返回原始字符串中的最长回文。接下来介绍一种可以返回删除或改组后的最长回文的算法。
如果一个字符串可以变成回文,那么它只有 $1$ 个或 $0$ 个字符数量是奇数。我们可以用哈希表记录每个字符出现的次数,然后把所有出现次数为奇数的字符都去掉一个,得到的字符串就是删除/改组后的最长回文。如果没有字符数量为奇数,把任意一个字符去掉一个也是回文。
def longest_palindrome(s):
freq = {}
for c in s:
freq[c] = freq.get(c, 0) + 1
odd_chars = [c for c in freq if freq[c] % 2 == 1]
if len(odd_chars) <= 1:
return s
for c in odd_chars:
s = s.replace(c, '', 1)
return s
这个算法的时间复杂度为 $O(n)$,因为只需要遍历字符串和哈希表。