📌  相关文章
📜  查找通过从字符串中删除或改组字符形成的最长回文(1)

📅  最后修改于: 2023-12-03 14:55:36.334000             🧑  作者: Mango

查找通过从字符串中删除或改组字符形成的最长回文

回文是指正着和反着读都一样的字符串。本文介绍如何查找通过从字符串中删除或改组字符形成的最长回文。

问题描述

给定一个字符串,查找通过从字符串中删除或改组字符形成的最长回文。例如,对于字符串 abcbda,可以删除字符 ad,来形成回文 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)$,因为只需要遍历字符串和哈希表。