【问题标题】:Why does this DP solution for longest common subsequence work correctly?为什么最长公共子序列的这种 DP 解决方案可以正常工作?
【发布时间】:2012-12-12 07:21:57
【问题描述】:

关于 longest common subsequence problem,所有在线资源中介绍的基本算法对我来说都很清楚。此处描述了此算法:

我不清楚的是为算法的动态编程版本提出的算法,它无处不在:

function LCSLength(X[1..m], Y[1..n])  
    C = array(0..m, 0..n)  
    for i := 0..m  
       C[i,0] = 0  
    for j := 0..n  
       C[0,j] = 0  
    for i := 1..m  
        for j := 1..n  
            if X[i] = Y[j]  
                C[i,j] := C[i-1,j-1] + 1  //Here shouldn't we change i?
            else  
                C[i,j] := max(C[i,j-1], C[i-1,j])  
    return C[m,n]   

但我看不出 DP 版本是如何等效的。令我困扰的是,在DP版本中,当我们在内循环中发现X[i] == Y[j]时,我们会继续使用相同的i计算DP;即内部循环的其余部分不断与相同的X[i] 进行比较。既然递归算法说我们应该计算 C[i - 1, j - 1],我们不应该继续下一个i吗?

【问题讨论】:

    标签: string algorithm data-structures dynamic-programming lcs


    【解决方案1】:

    动态规划背后的想法是逆向评估递归函数,从基本案例开始,迭代地构建越来越大的子问题的答案,直到计算出整体的答案输入问题。

    如果你递归地评估这个函数,那么你绝对会在你提到的情况下通过递减 i 和 j 来递归。但是,在动态编程版本中,您不是从 LCS[i, j] 开始并尝试通过评估 LCS[i-1, j-1] 来评估它。您从 LCS[i-1, j-1] 开始并使用它来评估 LCS[i, j]。

    具体来说,这段代码首先通过直接使用基本案例解决方案为所有 i 和 j 计算 LCS[i, 0] 和 LCS[0, j]。接下来,它使用所有 j 都知道 LCS[0, j] 的事实来计算所有 j 的 LCS[1, j]。然后它使用 LCS[1, j] 对所有 j 都已知这一事实来计算所有 j 的 LCS[2, j],依此类推。

    因此,是时候计算 LCS[i, j] 并且您的特定情况适用,该算法不需要递减 i 或 j 并递归地向下递减。它已经计算出 LCS[i-1, j-1],因此它可以读取该值并继续构建表中的其余值。

    这可能是最容易直观地看到的。假设您要查找字符串“canon”和“annie”的 LCS。我们从这个 2D 表格开始:

        A N N I E
      . . . . . .
    C . . . . . .
    A . . . . . .
    N . . . . . .
    O . . . . . .
    N . . . . . .
    

    最初,我们为所有 i 和 j 设置 LCS[0, j] = LCS[i, 0] = 0:

        A N N I E
      0 0 0 0 0 0
    C 0 . . . . .
    A 0 . . . . .
    N 0 . . . . .
    O 0 . . . . .
    N 0 . . . . .
    

    现在,我们将逐行浏览此表,并使用您在原始问题中描述的重复顺序填写缺失的条目。遍历第一行时,我们将比较字母 C 和单词“ANNIE”中的所有字母。我们永远找不到匹配项,所以我们总是使用递归 LCS[i, j] = max(LCS[i - 1, j] + LCS[i, j - 1])。这总是计算为零,所以我们得到:

        A N N I E
      0 0 0 0 0 0
    C 0 0 0 0 0 0
    A 0 . . . . .
    N 0 . . . . .
    O 0 . . . . .
    N 0 . . . . .
    

    这是有道理的,因为该表的第一行表示字符串 C 的 LCS 的长度以及 ANNIE 的所有前缀。

    在下一行中,我们将尝试查找字符串 CA 的 LCS 和 ANNIE 的所有后缀。我们考虑的第一个条目匹配 A 和 A。由于这是一个匹配,我们使用递归 LCS[i, j] = 1 + LCS[i - 1, j - 1],其计算结果为 1:

        A N N I E
      0 0 0 0 0 0
    C 0 0 0 0 0 0
    A 0 1 . . . .
    N 0 . . . . .
    O 0 . . . . .
    N 0 . . . . .
    

    同样,我们可以通过注意“CA”和“A”的 LCS 是长度为 1 的序列 A 来检查这一点。

    这里的重要细节是我们不会在此处减少 i 或 j。我们仍然需要填写此行的其余部分,因此我们将继续前进。

    对于这一行的其余条目,我们将 A 与 ANNIE 的每个字符进行比较,发现它不匹配。因此,我们将使用递归 LCS[i, j] = max(LCS[i-1, j], LCS[i, j-1]),它总是通过从其余部分中提取 1 来评估为 1行的。此处显示:

        A N N I E
      0 0 0 0 0 0
    C 0 0 0 0 0 0
    A 0 1 1 1 1 1
    N 0 . . . . .
    O 0 . . . . .
    N 0 . . . . .
    

    继续下一行为我们提供了以下信息:

        A N N I E
      0 0 0 0 0 0
    C 0 0 0 0 0 0
    A 0 1 1 1 1 1
    N 0 1 2 2 2 2
    O 0 . . . . .
    N 0 . . . . .
    

    同样,这是有道理的。 “CAN”和“A”的LCS就是“A”,“CAN”和“AN”的LCS就是“AN”,等等

    我们可以通过表格的其余部分重复此操作以找到此结果表格:

        A N N I E
      0 0 0 0 0 0
    C 0 0 0 0 0 0
    A 0 1 1 1 1 1
    N 0 1 2 2 2 2
    O 0 1 2 2 2 2
    N 0 1 2 3 3 3
    

    我们知道 LCS 的长度为 3,这是正确的。

    希望这会有所帮助!

    【讨论】:

    • 但是X[i] 将是所有内部for 循环中的相同元素,与Y 的元素相比,即使我们已经找到了匹配项。这让我感到困惑
    • @Cratylus- 没错。请记住,该算法试图根据所有 j 的 LCS[i-1, j] 的值来计算每个可能的 j 的 LCS[i, j]。结果,内部循环的每次迭代都将始终查看相同的 X[i],因为它试图根据 Y 的所有可能前缀找到 X 的前 i 个字符的 LCS。同样,请记住该算法正在填充在 DP 表中自下而上,而不是自上而下。
    【解决方案2】:

    C[i][j]的值取决于C[i][j-1]、C[i-1][j]和C[i-1][ j-1]。 因此,当我们在内循环中得到 C[i][j] 的值时,我们可以继续计算 C[i][j+1],因为 C[i][j] 和 C[i-1][ j+1] 和 C[i-1][j] 都已经计算过了。

    【讨论】:

    • 但是 X[i] 将是所有内部 for 循环中的相同元素,与 Y 的元素相比,即使我们已经找到了匹配项。这让我感到困惑
    • 这个二维数组的第i行用于计算A[1..i]和B[1..k]的LCS,其中1
    • 1<=k<=n n 是B 的大小吗?
    猜你喜欢
    • 1970-01-01
    • 2014-08-24
    • 1970-01-01
    • 2014-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多