【问题标题】:Finding the Longest Palindrome Subsequence with less memory用更少的内存找到最长的回文子序列
【发布时间】:2011-09-02 05:25:17
【问题描述】:

我正在尝试解决来自 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 通过链接候选子序列来实现这一界限,但对于回文则更难,因为这里重要的不是子序列中的前一个元素,而是第一个元素。有谁知道是否可以这样做,或者以前的解决方案内存是最优的吗?

【问题讨论】:

  • 给定输入 character,您的算法肯定会返回 ara,而不是 carac
  • @Ergwun 据我了解,回文可以来自任何保留原始顺序的子集,而不仅仅是来自连接的子集。
  • @Ergwun: carac 确实在 c h arac ter.
  • 啊,我不明白 subsequencesubstring 之间的区别 - 谢谢。
  • 子序列和子字符串的区别在于子字符串的元素必须出现在原始序列的连续索引处,而在子序列上,唯一的要求是保留它们的原始顺序。跨度>

标签: algorithm language-agnostic dynamic-programming palindrome


【解决方案1】:

这是一个非常节省内存的版本。但我还没有证明它是总是 O(n) 内存。 (通过预处理步骤,它可以比O(n<sup>2</sup>) CPU 更好,尽管O(n<sup>2</sup>) 是最坏的情况。)

从最左边的位置开始。对于每个位置,跟踪最远点的表格,在这些点上您可以生成长度为 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)]。 (这就是链表有用的地方。

通过列表的其余部分,我们不会改进那组界限。

所以我们最终得到最长的镜像列表的长度为 2。我们将按照链接列表(我没有在此描述中记录的内容找到它是 ac。因为该列表的末端是在位置(5, 3)我们可以翻转列表,插入字符4,然后追加列表得到carac

一般来说,它需要的最大内存是存储最大镜像子序列的所有长度加上存储所述子序列的链表的内存。通常这将是一个非常小的内存量。

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

【讨论】:

  • 如果您只想计算最佳回文的长度,您的方法非常节省内存。尽管在最坏的情况下它会占用O(n) 内存,但更好的界限是O(L),其中L 是最佳回文的长度。只是一个猜测,但我认为长度为n 的字符串的预期L 大约是O(sqrt(n))
  • 但我不太明白如何使用我们获得的列表重建最佳回文):
  • @Luiz Rodrigo:我有(length, end, start),你真的需要有(length, end, start, pointer_to_linked_list_for_subsequence)。该链表对子序列进行编码,当该子序列的尾部也出现在您的数据结构中时,可以廉价地共享它的片段。我在描述中省略了链接列表。你需要它来构建回文。
  • 哦,现在我明白了,这很简单。它确实需要一些记忆,但我认为它的平均表现会很好(我对理论方面不是很好)
  • @btilly 抱歉,我无法理解这一点。 character 是 9 个字母的单词。那么为什么镜像子序列会得到 (0,11) 呢?
【解决方案2】:

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

示例:对于字符串 CBACB,CAB 是字符串的 LCS 及其反转,它显然不是回文。 然而,有一种方法可以让它发挥作用。在构建字符串的 LCS 及其反向后,取其左半部分(包括奇数长度字符串的中间字符)并在右侧用反转的左半部分补足它(如果字符串长度为奇数则不包括中间字符)。 它显然是一个回文,并且可以简单地证明它是字符串的一个子序列。

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

【讨论】:

    猜你喜欢
    • 2011-06-15
    • 2018-05-13
    • 2020-04-14
    • 2012-10-05
    • 1970-01-01
    • 1970-01-01
    • 2017-02-25
    • 2020-03-25
    • 1970-01-01
    相关资源
    最近更新 更多