1

Longest Common Subsequence (LCS)问题是:给定两个序列A和,找到在和 中B都找到的最长子序列。例如,给定和,最长公共子序列是。ABA = "peterparker"B = "spiderman""pera"

有人可以解释这个Longest Common Subsequence算法吗?

def longestCommonSubsequence(A: List, B: List) -> int:
    # n = len(A)
    # m = len(B)
    
    indeces_A = collections.defaultdict(list)
    
    # O(n)
    for i, a in enumerate(A):
        indeces_A[a].append(i)
    
    # O(n)
    for indeces_a in indeces_A.values():
        indeces_a.reverse()
    
    # O(m)
    indeces_A_filtered = []
    for b in B:
        indeces_A_filtered.extend(indeces_A[b])
    
    # The length of indeces_A_filtered is at most n*m, but in practice it's more like O(m) or O(n) as far as I can tell.
    iAs = []
    # O(m log m) in practice as far as I can tell.
    for iA in indeces_A_filtered:
        j = bisect.bisect_left(iAs, iA)
        if j == len(iAs):
            iAs.append(iA)
        else:
            iAs[j] = iA
    return len(iAs)

所写的算法找到 的长度longest common subsequence,但可以修改以找到longest common subsequence完全的长度。

当我在 leetcode link上寻找一个等效问题的最快 python 解决方案时,我发现了这个算法。该算法是该问题最快的 Python 解决方案(40 毫秒),而且它似乎也具有时间复杂度,这比大多数其他解决方案O(m log m)的时间复杂度要好得多。O(m*n)

我不完全理解它为什么会起作用,并尝试到处寻找已知算法来Longest Common Subsequence解决问题以找到其他提及它的地方,但找不到类似的东西。我能找到的最接近的是Hunt–Szymanski algorithm 链接,据说O(m log m)在实践中也有,但似乎不是相同的算法。

我的理解是:

  1. indeces_a被反转,以便在iAsfor 循环中保留较小的索引(这在执行下面的演练时更加明显。)
  2. 据我所知,iAsfor 循环找到了longest increasing subsequenceof indeces_A_filtered

谢谢!


A = "peterparker"这是算法的演练,例如B = "spiderman"

     01234567890
A = "peterparker"
B = "spiderman"

indeces_A = {'p':[0,5], 'e':[1,3,9], 't':[2], 'r':[4,7,10], 'a':[6], 'k':[8]}

# after reverse
indeces_A = {'p':[5,0], 'e':[9,3,1], 't':[2], 'r':[10,7,4], 'a':[6], 'k':[8]}

#                     -p-  --e--  ---r--  a
indeces_A_filtered = [5,0, 9,3,1, 10,7,4, 6]

# the `iAs` loop

iA = 5
j = 0
iAs = [5]

iA = 0
j = 0
iAs = [0]

iA = 9
j = 1
iAs = [0,9]

iA = 3
j = 1
iAs = [0,3]

iA = 1
j = 1
iAs = [0,1]

iA = 10
j = 2
iAs = [0,1,10]

iA = 7
j = 2
iAs = [0,1,7]

iA = 4
j = 2
iAs = [0,1,4]

iA = 6
j = 3
iAs = [0,1,4,6] # corresponds to indices of A that spell out "pera", the LCS

return len(iAs) # 4, the length of the LCS
4

2 回答 2

1

这里缺少的部分是“耐心排序”,它与最长递增子序列(LIS)的联系有点微妙但众所周知。代码中的最后一个循环是使用“贪婪策略”耐心排序的基本实现。通常,它不直接计算 LIS,而是计算 LIS 的长度。

一个足够简单的正确性证明,其中包括可靠计算 LIS 所需内容的草图(不仅仅是它的长度),可以在早期的引理 1 中找到

“最长递增子序列:从耐心排序到 Baik-Deift-Johansson 定理” David Aldous 和 Persi Diaconis

于 2021-02-15T01:09:43.370 回答
0

了解 LIS 算法

def lenLIS(self, nums):
    lis = []
    for num in nums:
        i = bisect.bisect_left(lis, num)
        if i == len(lis):
            lis.append(num) # Append
        else:
            lis[i] = num # Overwrite
    return len(lis)

上面的算法给了我们 的最长递增子序列 (LIS) 的长度nums,但它不一定给我们LISnums。了解为什么上面的算法是正确的,以及如何修改它以继续LIS阅读nums

我通过例子来解释。


示例 1

nums = [7,2,8,1,3,4,10,6,9,5]
lenLIS(nums) == 5

算法告诉我们 a LISof的长度nums是 5,但是我们如何得到 a LISof nums

我们将历史表示lis如下(这将在下面解释):

7
2, 8
1, 3, 4, 10
          6, 9
          5

我们代表历史的方式lis是有意义的。首先,想象一个由行和列组成的空表。我们最初位于顶行。在循环的每次迭代中for num in nums:,我们要么Append Overwrite取决于 的值num和 的值lis

  • Append:我们在表中通过将附加值 ( num) 写入下一列(即i当前行的第 th 列)来表示这一点。附加值始终大于当前行中的所有值。
  • Overwrite: 如果lis[i]已经等于num,我们不对表做任何事情。否则,我们通过向下移动到下一行并在新行的第 th 列写入新值 ( num) 在表中表示这一点。i新值始终小于列中的所有其他值。

观察:

  1. 该表可以是稀疏的。
  2. 值按从左到右、从上到下的顺序插入。因此,每当我们在表格中向上或向左移动时,我们都会移动到nums.
  3. 当我们穿过一行时,值会增加。因此,向左移动会使我们的价值降低。
  4. 假设在 处有一个值,但在 处v没有。这只能作为覆盖的结果发生。考虑覆盖之前的状态。有一个值必须放入,我们将这个位置计算为。将计算 st 。从at ( ,我们可以通过向左移动一次(到空的)然后向上移动一次或多次直到我们遇到一个值来到达表中。那个值将是。(r, i)(r, i-1)lisvlisi = bisect_left(lis, v)ilis[i-1] < v < lis[i]vr, i)lis[i-1](r, i-1)lis[i-1]
  5. 综上所述2.3.我们已经表明,在表格中,我们总是可以向左移动一次,然后向上移动零次或多次以达到较小的值。将此运动的一个应用表示为prev。此外,1.告诉我们,我们通过执行遇到的较小值prev是较早出现的值nums

我们使用从表4.中获取。LIS(nums)从表中最右边的一个值开始,然后prev重复执行以逆序排列其他值LIS(nums)

在示例中执行此过程,我们从 开始9。申请prev一次,我们得到6。第二次,我们得到43然后1。确实[1,3,4,6,9]是其中LIS之一nums


示例 2:

nums = [2,7,10,14,25,5,6,5,10,20,1,22,4,12,7,11,9,25]
lenLIS(nums)

的历史lis

2, 7, 10, 14, 25
   5,  6  10, 20
1                22
   4          12
       7      11
           9     25

lenLIS(nums) == 6也是如此len(LIS(nums)。让我们找到LIS(nums)

同样,从表中最右边的值之一开始:22. 申请prev一次,我们得到20。第二次,我们得到10,然后6,然后5,然后2[2,5,6,10,20,22]一个LISof 也是如此nums

我们可以从其他最右边的值开始:25. 申请prev一次,我们得到11。第二次,我们得到10,然后6,然后5,然后2[2,5,6,10,11,25]另一个有效LIS的 也是如此nums


这个解释类似于https://www.stat.berkeley.edu/~aldous/Papers/me86.pdf中的解释,但我发现自己更容易理解,只是想分享它以防万一有用其他人。

于 2021-03-31T04:26:20.460 回答