【问题标题】:How and why does this code work? Finding the minimum number of steps to change one word to another这段代码如何以及为什么起作用?找到将一个单词更改为另一个单词的最小步骤数
【发布时间】:2017-04-23 12:39:40
【问题描述】:

我正在研究如何找到将 word1 转换为 word2 所需的最小步骤数,并遇到了以下带有规则的实现:

给定两个单词 word1 和 word2,找出将 word1 转换为 word2 所需的最小步数。 (每个操作都计为 1 步。)

您可以对一个单词进行以下 3 种操作:

a) 插入一个字符
b) 删除一个字符
c) 替换一个字符

而实现的思路是:

用distance[i][j]表示word1[0,i)和word2[0,j)之间的最短编辑距离。然后比较word1[0,i)和word2[0,j)的最后一个字符,分别是c和d(c == word1[i-1],d == word2[j-1]):

如果 c == d,则:距离[i][j] = 距离[i-1][j-1]

否则我们可以使用三个操作将word1转换为word2:

(a) 如果我们将 c 替换为 d:distance[i][j] = distance[i-1][j-1] + 1;

(b) 如果我们在 c 之后添加 d:distance[i][j] = distance[i][j-1] + 1;

(c) 如果我们删除 c: distance[i][j] = distance[i-1][j] + 1;

代码:

public class Solution {
public int minDistance(String word1, String word2) {
    int len1 = word1.length();
    int len2 = word2.length();

    //distance[i][j] is the distance converse word1(1~ith) to word2(1~jth)
    int[][] distance = new int[len1 + 1][len2 + 1]; 
    for (int j = 0; j <= len2; j++)
        {distance[0][j] = j;} //delete all characters in word2
    for (int i = 0; i <= len1; i++)
        {distance[i][0] = i;}

    for (int i = 1; i <= len1; i++) {
        for (int j = 1; j <= len2; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) { //ith & jth
                distance[i][j] = distance[i - 1][j - 1];
            } else {
                distance[i][j] = Math.min(Math.min(distance[i][j - 1], distance[i - 1][j]), distance[i - 1][j - 1]) + 1;
            }
        }
    }
    return distance[len1][len2];        
}
}

我的问题是 distance[][] 代表什么?为每个二维索引存储一个值有什么意义?为什么你在int[][] distance = new int[len1 + 1][len2 + 1]; 的len1 和len2 上加1?

例如,据我了解,它是将word1的每个字符与word2进行比较,但是一旦两个字符匹配,两个单词的索引不应该向上移动吗?意思是,如果String word1="ab";String word2="ac",由于a字符匹配,不需要比较word1中的a和word2中的c,而是上移索引,比较word1中的bc 在 word2 中。

最后,这三个操作如何代表它的方式,例如distance[i-1][j-1]怎么代表替换?

提前感谢您,我们将接受回答/赞成票。

【问题讨论】:

  • 如果我理解你正在搜索 LevenshteinDistance 可以thisthis 帮助你
  • @gori 感谢这些线索,但已经检查了它们,但在原始帖子中仍有相同的问题

标签: java algorithm dynamic-programming


【解决方案1】:

距离[][]代表什么?

表示minDistance(word1.substring(0, i), word2.substring(0, j))。这里 i 和 j 是子串的长度。

为每个二维索引存储一个值有什么意义? 这是动态规划的思想。部分解的答案计算一次,然后多次使用。如果您不将其存储在“全局”数组中,则每次需要时都必须计算它。对于这种情况,有 1-3 种可能的情况,因此递归计算可能需要 O((N*M)^3) 时间,其中 N 是 word1 的长度,M 是 word2 的长度。但是如果你只是简单地使用之前计算的结果,它只需要 O(N*M) 时间。

为什么在int[][] distance = new int[len1 + 1][len2 + 1]; len1 和 len2 上加1?

出于技术原因,您需要查看空子字符串的琐碎情况。为了存储这些情况,正在使用数组中的 distance[0][i] 和 distance[j][0] 插槽。

你可以用特殊情况计算来代替它(琐碎情况的解决方案是已知的),但它会使代码更复杂。您是否会使用递归调用而不是直接在数组中查找,这将是可行的。

一旦两个字符匹配,两个单词的索引不应该向上移动吗?

不,这不是关于移动索引,而是关于此时此地的部分解的计算。 i 和 j 的嵌套循环将在适当的时候关心“索引移动”。请记住,我们不是通过一个特别好的案例,而是计算 i=0..len1 和 j=0..len2 的所有部分解。每个部分解决方案仅在 1 或 3 个不同方向上退后 1 步。

最后,这三个操作如何代表它的方式,例如distance[i-1][j-1]怎么代表替换?

例如 minDistance("abc", "abd") = 1 + minDistance("ab", "ab") = 1 + minDistance("a", "a") = 1 + minDistance("", "") = 1 + 0 = 1。

本例以计算最终答案为例:

  • i=3="abc".length()
  • j=3="abd".length()
  • c = 'c' = "abc".charAt(i-1)
  • d = 'd' = "abd".charAt(j-1)

如果我们决定将 c 替换为 d,即 word1 中的最后一个字符到 word2 中的最后一个字符,我们使用已经计算出的答案来将单词的左侧部分缩短 1 个字符,因为替换将处理最后一个字符。我们将操作总数加 1,因为我们决定在这里进行替换。

【讨论】:

  • 非常感谢您的洞察力。不介意的话问几个问题。首先,您所说的琐碎案例是什么意思,距离[0][1]和距离[j][0]是什么意思?而且我仍然对 distance[i-1][j-1] 如何表示替换感到困惑。 i-1 和 j-1 不只是表示最后一个字符之前的一个字符吗?因此,如果 abc 和 abd,将是距离 [ab] [ab],但这如何表示替换?其他两个操作也是如此。如果你能用实际的 word1 和 word2 展示一个例子,那真的很有帮助。提前谢谢你
  • @JoKo distance[i-1][j-1] 并不意味着“最后一个字符”。意思是the optimal solution for changing the prefix of word1 ending at index (i-1) into the prefix of word2 ending at index (j-1).该表只是存储了将word1的不同大小前缀更改为word2的最优解。
  • @JoKo 想象将word1[0..5] 转换为word2[0..8]。想一想 - 如果您知道将 word1 的前四个字母转换为 word2 的前七个字母的最佳更改次数,并且您看到 word1 中的字母 5 与 word2 中的字母 8 不同,您可以使用该最佳解决方案d[4][7]加一个替换,将word1的前5个字母改成word2的前8个字母(前提是d[4][7]小于等于其他两个选项,d[5][7]d[4][8]
  • @JoKo 其他两个选择代表(1)首先将word1[0..5]转换为word[0..7],然后添加 word2的第8个字母(加1到所需更改的数量) ; (2) 将 word1[0..4] 转换为 word[0..8] 但为了使用该解决方案,我们必须删除字母 5(并将所需更改的数量加 1)。
  • @JoKo 决定一个解决方案是否最优的是它是否是一个较小的数字,因为这个问题是关于找到least number of steps 并且表格存储了它。选择使用来自d[4][7] 的解决方案(因为它小于或等于其他两个选择)意味着:Change the first 4 letters of word1 into the first 7 letters of word2 using the solution already stored in the table. Now replace the fifth letter of word1 so that it equals word2's eighth. 这将存储在d[5][8] 中作为代表替换的数字d[4][7] + 1。 (word2 永远不会被编辑。)
【解决方案2】:

为方便起见,我们将字长加 1,因为字符串索引对应于索引零处的 一些 长度,但我们需要在第一次比较时参考 distance[0-1][0-1]if word1[0] == word2[0]。如果没有额外的单元格,则分配 distance[i][j] = distance[i-1][j-1] 必须特别处理,而不仅仅是循环的一部分。

这种每次迭代都依赖于前一次迭代的结果的解决方案称为动态规划。让我们尝试对这个特定的规则表述进行描述。

首先,我们定义每个单元格代表什么:它是我们需要对以索引i 结尾的word1 的前缀应用的最小更改次数,以将该前缀更改为以索引@ 结尾的word2 的前缀987654325@。现在您可以看到准备工作,distance[i][0] = i 是如何有意义的 - 只需删除 i 即可将任何长度为 i 的前缀变成长度为零的字符串!

if c == d, then : distance[i][j] = distance[i-1][j-1]

翻译:由于我们什么都不做,所以使前缀长度ij 相同所需的更改次数将与使前两个前缀相等的更改次数相同,那些长度为@ 987654332@.

如果c 不等于d,我们将选择三个选项中较小的一个:

(a) if we replaced c with d: distance[i][j] = distance[i-1][j-1] + 1

翻译:假设此时我们的前缀长度相似,我们只是将i 的错误字符替换为与j 相同的字符。我们再次查看先前前缀长度[i-1][j-1] 的解决方案,但我们需要添加 1,因为我们进行了更改。 (现在请记住,这是我们将从三个选项中选择的一个。还请记住,任何先前的单元格都存储了到目前为止的最优解。)

(b) if we added d after c: distance[i][j] = distance[i][j-1] + 1

翻译:我们已经达到索引i,但它与j 不匹配,因此我们可以查看调整此前缀长度的最佳解决方案,使其匹配以(j-1) 结尾的那个(我们的解决方案已计算)并添加d,因此两个前缀都以正确的状态到达[i][j]。同样,我们需要在前一个状态的解上加 1。

(c) if we deleted c: distance[i][j] = distance[i-1][j] + 1

翻译:我们已经达到索引i,但它与j不匹配,因此我们可以查看调整先前前缀长度(我们已经计算过的i-1)的最佳解决方案,使其匹配一个以j 结尾,但我们需要加1,因为我们需要删除c 才能达到之前的前缀长度。

例子:

word1 = 'ab'
word2 = 'ac'

m = [[0,1,2]
    ,[1,0,1]
    ,[2,1,_]]

 (i,j)
 1,1 => m[i][j] = m[i-1][j-1] = 0 // no change needed

 1,2 => min(m[0][1],m[1,1],m[0,2]) + 1
      = min(1      ,0     ,2     ) + 1 
      = 1
 choice represented: easiest to change 'a' to 'ac' by adding
                     1 ('c') to the solution for [i][j-1] = [1][1]

 2,1 => min(m[1][0],m[2][0],m[1][1]) + 1
      = min(1,     ,2      ,0      ) + 1
      = 1
 choice represented: easiest to change 'ab' to 'a' by adding
                     1 (deletion) to the solution for [i-1][j] = [1][1]

 2,2 => min(m[1][1],m[2][1],m[1][2]) + 1
      = min(0      ,1      ,1      ) + 1
      = _
 choice represented: you figure it out...

【讨论】:

  • 非常感谢您的解释,但有些东西我还是不明白。寻找先前前缀长度的解决方案的原因是什么?如果c == d,那么不应该只是距离[i][j] = distance[i][j]? -1 引用前一个的原因是什么?对于情况(b),如果我们添加一个字符,它不应该是距离[i][j+1]吗?如果你能用两个词为每个案例展示一个例子,那真的很有帮助。提前谢谢你
  • @JoKo 谢谢你的评论。尝试再次阅读我的解释,并更仔细地考虑每个陈述。我为您的问题中的示例添加了插图。
猜你喜欢
  • 2014-04-30
  • 2014-06-18
  • 1970-01-01
  • 2010-12-04
  • 2012-07-15
  • 2021-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多