问题
I'm researching on how to find the minimum number of steps required to convert word1 to word2, and came across the following implementation with the rules:
Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2. (each operation is counted as 1 step.)
You have the following 3 operations permitted on a word:
a) Insert a character
b) Delete a character
c) Replace a character
And the idea of the implementation is:
Use distance[i][j] to represent the shortest edit distance between word1[0,i) and word2[0, j). Then compare the last character of word1[0,i) and word2[0,j), which are c and d respectively (c == word1[i-1], d == word2[j-1]):
if c == d, then : distance[i][j] = distance[i-1][j-1]
Otherwise we can use three operations to convert word1 to word2:
(a) if we replaced c with d: distance[i][j] = distance[i-1][j-1] + 1;
(b) if we added d after c: distance[i][j] = distance[i][j-1] + 1;
(c) if we deleted c: distance[i][j] = distance[i-1][j] + 1;
Code:
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];
}
}
And my question is what does distance[][] represent? What's the point of storing a value for every 2D index? And why do you add 1 to len1 and len2 in int[][] distance = new int[len1 + 1][len2 + 1];?
For example, from my understanding, it is comparing each character of word1 to word2, but once both characters match, shouldn't both words' indexes move up? Meaning, if String word1="ab"; and String word2="ac", since a characters match, there is no need to compare a in word1 to c in word2, but rather move up indexes, and compare b in word1 to c in word2.
Lastly, how do the three operations represent the way that do, e.g. how come distance[i-1][j-1] represent replacement?
Thank you in advance and will accept answer/up vote.
回答1:
what does distance[][] represent?
It represents minDistance(word1.substring(0, i), word2.substring(0, j)). Here i and j are lengthes of substrings.
What's the point of storing a value for every 2D index? It's dynamic programming idea. The answer for partial solution is calculated once and then used multiple times. If you don't store it in "global" array, you have to calculate it every time you need it. For this case there are 1-3 possible cases, so recursive calculations could take O((N*M)^3) time, where N is length of word1 and M is length of word2. But if instead you simply use previously calculated result, it will only take O(N*M) time.
And why do you add 1 to len1 and len2 in int[][] distance = new int[len1 + 1][len2 + 1];?
For technical reasons you need to look up back to trivial cases of empty substrings. To store these cases distance[0][i] and distance[j][0] slots in array are being used.
You can replace it with special case calculations (the solution for trivial cases is known), but it will make code more complex. Would you go for recursive calls instead of direct look ups in array, it would be viable.
once both characters match, shouldn't both words' indexes move up?
No, it's not about moving index, it's about calculation of partial solution here and now. Nested loops for i and j will care about "index moving" in appropriate time. Remember, we are not going through one particulary good case, but calculate all partial solutions for i=0..len1 and j=0..len2. Each partial solution only makes 1 step back in 1 or 3 different directions.
Lastly, how do the three operations represent the way that do, e.g. how come distance[i-1][j-1] represent replacement?
For example, minDistance("abc", "abd") = 1 + minDistance("ab", "ab") = 1 + minDistance("a", "a") = 1 + minDistance("", "") = 1 + 0 = 1.
In this example for the case of calculation of final answer:
- i=3="abc".length()
- j=3="abd".length()
- c = 'c' = "abc".charAt(i-1)
- d = 'd' = "abd".charAt(j-1)
If we decide to replace c to d, that is last character in word1 to last character in word2, we use already calculated answer for left parts of words 1 character shorter, since replacement will take care of last character. We add 1 to total number of operations, since we decided to make this replacement here.
回答2:
We're adding 1 to the word lengths for convenience since string indexing corresponds with some length at index zero, but we need to refer to distance[0-1][0-1] on the first comparison, if word1[0] == word2[0]. Without an extra cell, the assignment, distance[i][j] = distance[i-1][j-1], would have to be especially addressed rather than just be part of the loop.
This kind of solution formulation, where each iteration relies on a previous iteration's result is called dynamic programming. Let's try to put words to this particular rule formulation.
First of all, we define what each cell represents: its the smallest number of changes we need to apply to the prefix of word1 that ends at index i to change that prefix to the prefix of word2 that ends at index j. Now you can see how the preparation, distance[i][0] = i makes sense - it would take exactly i deletions to make any prefix of length i into a string of length zero!
if c == d, then : distance[i][j] = distance[i-1][j-1]
Translation: since we had to change nothing, the number of changes it took to make prefix length i the same as j would be the same number of changes to get the previous two prefixes equal, those with lengths [i-1][j-1].
If c does not equal d, we are going to choose the smaller of three options:
(a) if we replaced c with d: distance[i][j] = distance[i-1][j-1] + 1
Translation: imagine our prefixes are of similar length at this point and we just replaced the wrong character at i to be the same as the one at j. Again we look at the solution for the previous prefix lengths [i-1][j-1] but we need to add 1 since we made a change. (Now remeber this is one option of three that we will choose from. Also remember that any previous cell stores the optimal solution up to that point.)
(b) if we added d after c: distance[i][j] = distance[i][j-1] + 1
Translation: we've reached index i but it doesn't match j, therefore we can look at the optimal solution for adjusting this prefix length so it matches the one ending at (j-1) (a solution we already computed) and add the d so both prefixes reach [i][j] in a correct state. Again we need to add 1 to the previous state's solution.
(c) if we deleted c: distance[i][j] = distance[i-1][j] + 1
Translation: we've reached index i but it doesn't match j, therefore we can look at the optimal solution for adjusting the previous prefix length (i-1 which we already computed) so it matches the one ending at j, but we need to add 1 since we need to remove c to reach the previous prefix length.
Example:
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...
来源:https://stackoverflow.com/questions/41036622/how-and-why-does-this-code-work-finding-the-minimum-number-of-steps-to-change-o