这个算法在我的书中被称为字符串匹配。它在 O(mn) 中运行,其中 m 和 n 是单词长度。我想它也可以在完整的单词上运行,最有效的将取决于预期的常见字母数量以及如何执行排序和过滤。我会为常见的字母字符串解释它,因为这样更容易。
这个想法是您查看 (m+1)*(n+1) 个节点的有向无环图。通过该图的每条路径(从左上角到右下角)都代表一种匹配单词的独特方式。我们要匹配字符串,并在单词中添加空格(-),以便它们与最多的常用字母对齐。例如cay 和ayc 的结束状态是
cay-
-ayc
每个节点存储它所代表的部分匹配的最大匹配数,并且在算法结束时,末端节点将为我们提供最大匹配数。
我们从左上角开始,这里没有任何匹配,所以我们这里有 0 个匹配的字母(得分 0)。
c a y
0 . . .
a . . . .
y . . . .
c . . . .
我们将遍历此图,并使用来自先前节点的数据为每个节点计算匹配字母的最大数量。
节点连接左->右、上->下和对角-左上->右-下。
- 向右移动表示消耗来自
cay 的一个字母,并将我们到达的字母与插入ayc 的- 匹配。
- 向下移动代表相反的情况(从
ayc 消费并插入- 到cay)。
- 沿对角线移动表示从每个单词中提取一个字母并匹配这些字母。
查看我们起始节点右侧的第一个节点,它表示匹配
c
-
并且这个节点(显然)只能从起始节点到达。
第一行和第一列中的所有节点都将为 0,因为它们都表示匹配一个或多个具有相同数量的 - 的字母。
我们得到图表
c a y
0 0 0 0
a 0 . . .
y 0 . . .
c 0 . . .
这就是设置,现在有趣的部分开始了。
查看第一个未计算的节点,它表示将子字符串 c 与 a 匹配,我们想决定如何在匹配字母最多的情况下到达那里。
- 备选方案 1:我们可以从左侧的节点到达那里。左边的节点代表匹配
-
a
所以通过选择这条路径来到达我们当前的节点
-c
a-
将c 与- 匹配不会给我们正确的匹配,因此这条路径的分数是0(取自最后一个节点)加上0(刚刚匹配c/- 的分数)。所以这条路径的 0 + 0 = 0。
- 方案2:我们可以从上面到这个节点,这个路径代表从上面移动
c -> c-
- -a
这也给了我们0分。得分为 0。
- 备选方案 3:我们可以从左上角到达该节点。这是从起始节点(根本没有)移动到从每个字母中消耗一个字符。那是匹配的
c
a
由于c 和a 是不同的字母,因此我们也得到此路径的 0 + 0 = 0。
c a y
0 0 0 0
a 0 0 . .
y 0 . . .
c 0 . . .
但是对于下一个节点,它看起来更好。我们仍然有这三种选择。
备选方案 1 和 2 总是给我们额外的 0 分,因为它们总是表示将字母与 - 匹配,所以这些路径会给我们得分 0。让我们继续备选方案 3。
对于我们当前的节点沿对角线移动意味着从
c -> ca
- -a
这是一场比赛!
这意味着有一条通往该节点的路径给我们 1 分。我们扔掉 0 并保存 1。
c a y
0 0 0 0
a 0 0 1 .
y 0 . . .
c 0 . . .
对于这一行的最后一个节点,我们查看了三个备选方案,并意识到我们不会得到任何新点(新匹配),但我们可以使用之前的 1 点路径到达该节点:
ca -> cay
-a -a-
所以这个节点的分数也是1。
对所有节点执行此操作,我们得到以下完整图
c a y
0 0 0 0
a 0 0 1 1
y 0 0 1 2
c 0 1 1 2
分数的唯一增加来自哪里
c -> ca | ca -> cay | - -> -c
- -a | -a -ay | y yc
所以结束节点告诉我们最大匹配是 2 个字母。
由于在您的情况下您希望知道得分为 2 的最长路径,因此您还需要跟踪每个节点所采用的路径。
此图很容易实现为矩阵(或数组数组)。
我建议您作为元素使用tuple 和一个score 元素和一个path 元素,并且在路径元素中您只需存储对齐字母,那么最终矩阵的元素将是
c a y
0 0 0 0
a 0 0 (1, a) (1, a)
y 0 0 (1, a) (2, ay)
c 0 (1, c) (1, a/c) (2, ay)
在一个地方我注意到a/c,这是因为字符串ca 和ayc 有两个不同的最大长度子序列。您需要决定在这些情况下该怎么做,要么选择一个,要么两个都保存。
编辑:
这是此解决方案的实现。
def longest_common(string_1, string_2):
len_1 = len(string_1)
len_2 = len(string_2)
m = [[(0,"") for _ in range(len_1 + 1)] for _ in range(len_2 + 1)] # intitate matrix
for row in range(1, len_2+1):
for col in range(1, len_1+1):
diag = 0
match = ""
if string_1[col-1] == string_2[row-1]: # score increase with one if letters match in diagonal move
diag = 1
match = string_1[col - 1]
# find best alternative
if m[row][col-1][0] >= m[row-1][col][0] and m[row][col-1][0] >= m[row-1][col-1][0]+diag:
m[row][col] = m[row][col-1] # path from left is best
elif m[row-1][col][0] >= m[row-1][col-1][0]+diag:
m[row][col] = m[row-1][col] # path from above is best
else:
m[row][col] = (m[row-1][col-1][0]+diag, m[row-1][col-1][1]+match) # path diagonally is best
return m[len_2][len_1][1]
>>> print(longest_common("hcarry", "sallyc"))
ay
>>> print(longest_common("cay", "ayc"))
ay
>>> m
[[(0, ''), (0, ''), (0, ''), (0, '')],
[(0, ''), (0, ''), (1, 'a'), (1, 'a')],
[(0, ''), (0, ''), (1, 'a'), (2, 'ay')],
[(0, ''), (1, 'c'), (1, 'c'), (2, 'ay')]]