用更少的内存找到最长的Palindrome子序列

时间:2011-05-26 01:07:44

标签: algorithm language-agnostic dynamic-programming palindrome

我正在尝试解决Cormem的Introduction to Algorithms 3rd edition(第405页)中的动态编程问题,该问题询问以下内容:

  

回文是一个非空的字符串   一些字母相同的字母表   前进和后退。示例   回文都是长度的串   1,civicracecaraibohphobia   (害怕回文)。

     

提供有效的算法来查找   最长的回文是一个   给定输入字符串的子序列。   例如,给定输入   character,你的算法应该   返回carac

好吧,我可以通过两种方式解决它:

第一个解决方案:

字符串的最长回文子序列(LPS)只是其本身的Longest Common Subsequence及其反向。 (我在解决了另一个要求序列的Longest Increasing Subsequence的相关问题之后构建了这个解决方案。 由于它只是一个LCS变体,它还需要O(n²)时间和O(n²)存储器。

第二个解决方案:

第二个解决方案稍微详细一点,但也遵循一般的LCS模板。它来自以下重现:

lps(s[i..j]) = 
    s[i] + lps(s[i+1]..[j-1]) + s[j], if s[i] == s[j];
    max(lps(s[i+1..j]), lps(s[i..j-1])) otherwise

用于计算lps长度的伪代码如下:

compute-lps(s, n):

    // palindromes with length 1
    for i = 1 to n:
        c[i, i] = 1
    // palindromes with length up to 2
    for i = 1 to n-1:
        c[i, i+1] = (s[i] == s[i+1]) ? 2 : 1

    // palindromes with length up to j+1
    for j = 2 to n-1:
        for i = 1 to n-i:
            if s[i] == s[i+j]:
                c[i, i+j] = 2 + c[i+1, i+j-1]
            else:
                c[i, i+j] = max( c[i+1, i+j] , c[i, i+j-1] )

如果我想有效地构造 lps(因为我需要表中的所有单元格),它仍需要O(n²)时间和内存。分析相关的问题,比如LIS,可以通过LCS之外的方法解决,内存较少(LIS可以用O(n)内存解决),我想知道是否有可能用O(n)内存来解决它,太

LIS通过链接候选子序列来实现这个限制,但是对于回文来说,它更难,因为这里重要的不是后续元素,而是第一个元素。有谁知道是否有可能做到这一点,或者以前的解决方案内存是否最佳?

2 个答案:

答案 0 :(得分:6)

这是一个内存效率非常高的版本。但我没有证明它总是总是 O(n)内存。 (通过预处理步骤,它可以比O(n2) CPU更好,但O(n2)是最糟糕的情况。)

从最左边的位置开始。对于每个位置,跟踪最远点的表格,您可以在该表格中生成长度为1,2,3等的反射子序列(意味着我们点左侧的子序列将反映到右侧。)每个反射的子序列我们存储一个指向子序列下一部分的指针。

当我们正确地工作时,我们从字符串的RHS搜索当前元素的任何出现的位置,并尝试使用这些匹配来改善我们以前的边界。当我们完成时,我们会看到最长的镜像子序列,我们可以很容易地构建最好的回文。

让我们考虑一下character

  1. 我们从最好的回文开始是字母'c',我们的镜像子序列是通过字符串两端的(0, 11)对达到的。
  2. 接下来考虑位置1处的“c”。我们(length, end, start)形式的最佳镜像子序列现在是[(0, 11, 0), (1, 6, 1)]。 (我将省略你需要生成的链表,以便真正找到回文。
  3. 接下来考虑位置2的h。我们不会改善界限[(0, 11, 0), (1, 6, 1)]
  4. 接下来考虑位置3的a。我们改进了[(0, 11, 0), (1, 6, 1), (2, 5, 3)]的界限。
  5. 接下来考虑位置4的r。我们改进了[(0, 11, 0), (1, 10, 4), (2, 5, 3)]的界限。 (这是链表有用的地方。
  6. 通过列表的其余部分,我们不会改进这组界限。

    所以我们最长的镜像列表长度为2.我们会按照链接列表(我没有在此描述中记录,发现它是ac。从那以后列表位于(5, 3)位置我们可以翻转列表,插入字符4,然后附加列表以获取carac

    通常,它需要的最大内存是存储最大镜像子序列的所有长度加上存储器以存储所述子序列的链接列表。通常情况下,这将是一个非常小的内存。

    在典型的内存/ CPU权衡中,您可以对列表进行一次预处理O(n),以生成O(n)大小的数组散列,其中显示特定序列元素的位置。这可以让您扫描“使用此配对改进镜像子序列”,而无需考虑整个字符串,对于较长的字符串,这通常应该是CPU的主要节省。

答案 1 :(得分:3)

@Luiz Rodrigo的问题中的第一个解决方案是错误的:字符串的最长公共子序列(LCS)及其反向不一定是回文。

示例:对于字符串CBACB,CAB是字符串的LCS及其反向,它显然不是回文。 但是,有一种方法可以使它发挥作用。在构建一个字符串的LCS并将其反向构建之后,取其左半部分(包括奇数长度字符串的中间字符)并在右侧补充它,使用反向左半部分(如果字符串的长度为奇数,则不包括中间字符) )。 它显然是一个回文,它可以被轻易证明它将是字符串的后续部分。

对于上述LCS,以这种方式构建的回文将是CAC。