【问题标题】:Can anyone explain this code for computing Levenshtein distance?谁能解释这个计算Levenshtein距离的代码?
【发布时间】:2021-11-01 03:24:39
【问题描述】:

我得到了这段代码,它可以快速返回两个字符串之间的Levenshtein distance 是否正好是 2。

def li(s, i):
    try:
        return s[i]
    except IndexError:
        return None
    
def f(str1, str2):
 t = [4, 4, 1, 2, 3]
 for i, str1_symb in enumerate(str1):
    p = 4
    res = []
    for j, t_val in enumerate(t):
        p = min(t_val - (str1_symb == li(str2, i + j - 2)), p, li(t, j + 1) or 4) + 1
        res.append(p)
    t = res
 return li(t, len(str2) - len(str1) + 2) == 3

您可以使用以下方法对其进行测试:

f("zzzzfood", "zzzzdodod") 

例如将返回True

f("zzzzfood", "zzzzdodo")

这将返回 False。

计算 Levenshtein 距离的标准算法构建一个动态规划表,并使用公式从左到右、从上到下填充元素:

(来自上面链接的 wiki 页面)

如果您只想在 Levenshtein 距离最多为 2 的情况下返回,则只能查看动态规划的单元格,该单元格距离对角线的右侧或左侧最多 2 个。

上面的代码显然没有这样做,我无法弄清楚它在做什么。一些特别神秘的部分:

  • t = [4, 4, 1, 2, 3]的作用是什么?
  • li() 函数在此代码中同时获取字符串和列表。仅当索引 i 大于或等于 len(s) 时才返回 None。有时i 会是负数,它仍然会返回来自s 的信。
  • 如果li(t, j + 1)Noneli(t, j + 1) or 4 返回 4,但我不知道它的用途是什么。
  • p 的目的/含义是什么?

谁能破解?

【问题讨论】:

  • 这是非常糟糕的代码。变量的名字很糟糕,所以很难理解它们应该做什么。
  • 有比这个更好的代码来阅读和理解......这是一个经典 - 所以应该不难找到它。
  • 为什么还要解码乱码;写一个更容易理解的实现。
  • "如果你只想在 Levenshtein 距离最多为 2 的情况下返回,你只能查看动态规划中距离对角线最多 2 个左右的单元格。" 注意如果要检查两个单词ab是否在距离2,然后计算距离为2的单词集合d2aa并检查b是否在其中效率很低。计算距离a 距离为1 的单词和距离b 距离为1 的单词的两组d1ad1b 会更有效,然后看看这两组中是否有共同的单词。
  • @Stef 代码根本不计算d2a。这不是动态规划表的作用。

标签: python algorithm levenshtein-distance


【解决方案1】:

好的——首先,请不要使用这段代码。

在高层次上,它所做的就是 Stef 在 cmets 中所说的,检查近对角线。 i 的索引位置正在迭代,循环的 j 正在穿过靠近对角线的第二个字符串

添加一些打印会使这更清晰:

def f(str1, str2):
    t = [4, 4, 1, 2, 3]
    for i, str1_symb in enumerate(str1):
        print()
        print(f"i={i}, str1_symb={str1_symb}")
        p = 4
        res = []
        print("res:", res)
        for j, t_val in enumerate(t):
            print()
            print(f" - j={j}, t_val={t_val}")
            p1 = t_val - (str1_symb == li(str2, i + j - 2))
            p2 = p
            p3 = li(t, j + 1) or 4
            print(f" - p1a: t_val - (str1_symb == li(str2, i + j - 2))  ==>  {t_val} - ({str1_symb} == li({str2}, {i} + {j} - 2))")
            print(f" - p1b: t_val - (str1_symb == li(str2, i + j - 2))  ==>  {t_val} - ({str1_symb} == li({str2}, {i + j - 2}))")
            print(f" - p1c: t_val - (str1_symb == li(str2, i + j - 2))  ==>  {t_val} - ({str1_symb} == {li(str2, i + j - 2)})")
            print(f" - p1d: t_val - (str1_symb == li(str2, i + j - 2))  ==>  {t_val} - {(str1_symb == li(str2, i + j - 2))}")
            print(f" - p1e: t_val - (str1_symb == li(str2, i + j - 2))  ==>  {p1}")
            print(" - ")
            print(f" - p2: {p}")
            print(f" - ")
            print(f" - p3a: li(t, j + 1) or 4 ==> li({t}, {j} + 1) or 4")
            print(f" - p3b: li(t, j + 1) or 4 ==> li({t}, {j + 1}) or 4")
            print(f" - p3c: li(t, j + 1) or 4 ==> {li(t, j + 1)} or 4")
            print(f" - p3: {li(t, j + 1) or 4}")
            print(f" - ")
            p = min(p1, p2, p3) + 1
            print(f" - p: min(p1, p2, p3) + 1 ==> min({p1}, {p2}, {p3}) + 1")
            print(f" - p: min(p1, p2, p3) + 1 ==> {min(p1, p2, p3) + 1}")
            res.append(p)
        t = res
        print(f"t = {t}")
    print()
    print(f"result: li(t, len(str2) - len(str1) + 2) == 3 ==> li({t}, {len(str2)} - {len(str1)} + 2) == 3")
    print(f"result: li(t, len(str2) - len(str1) + 2) == 3 ==> li({t}, {len(str2) - len(str1) + 2}) == 3")
    print(f"result: li(t, len(str2) - len(str1) + 2) == 3 ==> {li(t, len(str2) - len(str1) + 2)} == 3")
    print(f"result: li(t, len(str2) - len(str1) + 2) == 3 ==> {li(t, len(str2) - len(str1) + 2) == 3}")
    return li(t, len(str2) - len(str1) + 2) == 3

我对@9​​87654328@ 的阅读是它正在设置偏移值和最大值(这样我们就可以有效地忽略 -ive 列表索引值,任何大于 3 的值都应该产生相同的结果) 负面清单索引是多余的计算,但无害。

我们可以看到 i & j 如何在矩阵中移动:

产生以下差分矩阵:

将 p 计算 p = min(t_val - (str1_symb == li(str2, i + j - 2)), p, li(t, j + 1) or 4) + 1 分成三个部分,使其工作更加清晰:

p1 = t_val - (str1_symb == li(str2, i + j - 2))
p2 = p
p3 = li(t, j + 1) or 4
p = min(p1, p2, p3) + 1

我们可以看到这一步使用t/res作为最后一次计算的内存,我们在其中查找先前计算的偏移量并计算新值

最后一点是,看起来一切都被1放大了,所以作者可以使用-true来表示不同;然后将其添加回min,它将2 的距离缩放到3 的距离,导致最终相等


这太难读了,我不断获得一些理解,然后失去了想法,这是一个有趣的逆向工程问题,但生产代码很糟糕。


当我花了一点时间试图想出一个更好的方法时,我意识到我最终走上了同样的道路;尽管我对可读性提出了批评,但我必须为作者的效率提供支持。计算和存储其他条件的算法并执行它是聪明而高效的。

【讨论】:

  • 刚刚注意到我不小心在表中索引了 j —— excel 格式错误,不代表代码在做什么;我会尽快更新
  • 谢谢你。您认为您可以添加具有相同时间复杂度的更健全的 Python 等效项吗?
  • @donald 老实说,不,我认为没有更有效的方法来处理这个问题。我们可以将返回 None 与返回 4 交换以避免 ors 并且如果 i 是 -ive 也可以更新 li 以返回 4,但这与我认为有人会得到的效率差不多。我对它的批评是可读性,而不是方法。为此,我会使用真正的变量名(没有理由不这样做)并去掉所有的幻数,但仅此而已
  • 如果您有精力写出一个更易于阅读的 Python 解决方案,我将不胜感激。
  • 当我不小心关闭了未保存的笔记本时,我已经完成了 90% 的使用干净打印的更干净的解释 [在尝试决定如何命名初始 tval 行时有点卡住了]。除非有人超过我,否则我会尝试绕回它
猜你喜欢
  • 2013-01-15
  • 2014-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-27
  • 1970-01-01
  • 2023-03-23
相关资源
最近更新 更多