微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

有人可以解释这个“最长公共子序列”算法吗? 示例 1观察:示例 2:

如何解决有人可以解释这个“最长公共子序列”算法吗? 示例 1观察:示例 2:

Longest Common Subsequence (LCS) 问题是:给定两个序列 AB,找出在 AB 中都找到的最长子序列。例如,给定 A = "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 link,据说它在实践中也有 O(m log m),但似乎不是相同的算法。

我的理解:

  1. indeces_a 被反转,以便在 iAs for 循环中,保留较小的索引(在执行下面的演练时这一点更为明显。)
  2. 据我所知,iAs for 循环查找 longest increasing subsequenceindeces_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,1],'r':[10,4],'k':[8]}

#                     -p-  --e--  ---r--  a
indeces_A_filtered = [5,9,1,10,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,10]

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

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

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

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

解决方法

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

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

"Longest Increasing Subsequences: From Patience Sorting to the Baik-Deift-Johansson Theorem" David Aldous 和 Persi Diaconis

,

了解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)

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

我通过例子来解释。


示例 1

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

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

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

7
2,8
1,10
          6,9
          5

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

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

观察:

  1. 表格可能很稀疏。
  2. 值按从左到右、从上到下的顺序插入。因此,每当我们在表格中向上或向左移动时,我们都会移动到 nums 的较早元素。
  3. 当我们穿过一行时,值会增加。因此,向左移动会降低我们的价值。
  4. 假设在 v 处有一个值 (r,i),但在 (r,i-1) 处没有。这只能作为覆盖的结果发生。考虑覆盖之前 lis 的状态。有一个值 v 必须放在 lis 中,我们将这个位置计算为 i = bisect_left(lis,v)i 将被计算 s.t. lis[i-1] < v < lis[i]。从 (v 处的 r,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。第二次,我们得到 4,然后是 3,然后是 1。实际上 [1,9]LISnums 之一。


示例 2:

nums = [2,7,14,25,5,20,22,12,11,25]
lenLIS(nums)

lis 的历史:

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

所以 lenLIS(nums) == 6len(LIS(nums)。让我们找到LIS(nums)

再次从表中最右边的值之一开始:22。应用 prev 一次,我们得到 20。第二次,我们得到 10,然后是 6,然后是 5,然后是 2。所以 [2,22]LISnums

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


这个解释与https://www.stat.berkeley.edu/~aldous/Papers/me86.pdf中的类似,但我觉得它更容易理解,只是想分享一下,以防对其他人有用。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。