问题描述
Longest Common Subsequence (LCS)
问题是:给定两个序列 A
和 B
,找出在 A
和 B
中都找到的最长子序列。例如,给定 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)
,但似乎不是相同的算法。
我的理解:
-
indeces_a
被反转,以便在iAs
for 循环中,保留较小的索引(在执行下面的演练时这一点更为明显。) - 据我所知,
iAs
for 循环查找longest increasing subsequence
的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,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) 的长度,但它不一定为我们提供了 LIS
的 nums
。了解为什么上述算法是正确的,以及如何修改它以继续阅读 LIS
的 nums
。
我通过例子来解释。
示例 1
- (取自由 Tim Peters 链接的 https://www.stat.berkeley.edu/~aldous/Papers/me86.pdf):
nums = [7,2,8,1,3,4,10,6,9,5]
lenLIS(nums) == 5
算法告诉我们 LIS
的 nums
的长度是 5,但是我们如何得到 LIS
的 nums
。
我们表示 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
) 在表中表示这一点。新值始终小于列中的所有其他值。
观察:
- 表格可能很稀疏。
- 值按从左到右、从上到下的顺序插入。因此,每当我们在表格中向上或向左移动时,我们都会移动到
nums
的较早元素。 - 当我们穿过一行时,值会增加。因此,向左移动会降低我们的价值。
- 假设在
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]
。 - 将
2.
和3.
放在一起,我们已经证明,在表格中,我们总是可以向左移动一次,然后向上移动零次或多次以达到较小的值。将此运动的一个应用表示为prev
。此外,1.
告诉我们执行prev
时遇到的较小值是在nums
中较早出现的值。
我们使用 4.
从表中获取 LIS(nums)
。从表格最右边的值之一开始,然后重复执行 prev
以反向遇到 LIS(nums)
的其他值。
对示例执行此过程,我们从 9
开始。应用 prev
一次,我们得到 6
。第二次,我们得到 4
,然后是 3
,然后是 1
。实际上 [1,9]
是 LIS
的 nums
之一。
示例 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) == 6
是 len(LIS(nums)
。让我们找到LIS(nums)
:
再次从表中最右边的值之一开始:22
。应用 prev
一次,我们得到 20
。第二次,我们得到 10
,然后是 6
,然后是 5
,然后是 2
。所以 [2,22]
是 LIS
的 nums
。
我们可以从其他最右边的值开始:25
。应用 prev
一次,我们得到 11
。第二次,我们得到 10
,然后是 6
,然后是 5
,然后是 2
。所以 [2,25]
是 LIS
的另一个有效 nums
。
这个解释与https://www.stat.berkeley.edu/~aldous/Papers/me86.pdf中的类似,但我觉得它更容易理解,只是想分享一下,以防对其他人有用。