我知道在堆栈上也有类似的答案,以及在线,但我觉得我错过了一些东西。鉴于下面的代码,我们需要重建导致最小编辑距离的事件序列。对于下面的代码,我们需要编写一个输出函数:
Equal, L, L
Delete, E
Equal, A, A
Substitute, D, S
Insert, T
编辑:我的(部分正确)解决方案更新了代码
这是代码,我的部分解决方案。它的作用例如我被给予(“引导” - >“最后”),但不适用于下面的例子(“提示” - >“不是”)。我怀疑这是因为第一个字符相同,这就是抛弃我的代码。任何正确方向的提示或指示都会很棒!
def printMatrix(M):
for row in M:
print row
print
def med(s, t):
k = len(s) + 1
l = len(t) + 1
M = [[0 for i in range(k)] for j in range(l)]
MTrace = [["" for i in range(k)] for j in range(l)]
M[0][0] = 0
for i in xrange(0, k):
M[i][0] = i
MTrace[i][0] = s[i-1]
for j in xrange(0, l):
M[0][j] = j
MTrace[0][j] = t[j-1]
MTrace[0][0] = "DONE"
for i in xrange(1, k):
for j in xrange(1, l):
sub = 1
sub_op = "sub"
if s[i-1] == t[j-1]:
# equality
sub = 0
sub_op = "eq"
# deletion
min_value = M[i-1][j] + 1
op = "del"
if min_value > M[i][j-1] + 1:
# insertion
min_value = M[i][j-1] + 1
op = "ins"
if min_value > M[i-1][j-1] + sub:
# substitution
min_value = M[i-1][j-1] + sub
op = sub_op
M[i][j] = min_value
MTrace[i][j] = op
print "final Matrix"
printMatrix(M)
printMatrix(MTrace)
############ MY PARTIAL SOLUTION
def array_append(array,x,y):
ops_string = MTrace[x][y]
if ops_string == 'ins':
array.append(("Insert",MTrace[0][y]))
elif ops_string == 'sub':
array.append(("Substitute",MTrace[x][0],MTrace[0][y]))
elif ops_string == 'eq':
array.append(("Equal",MTrace[x][0],MTrace[0][y]))
elif ops_string == 'del':
array.append(("Delete",MTrace[x][0]))
i = len(s)
j = len(t)
ops_array = []
base = M[i][j]
array_append(ops_array,i,j)
while MTrace[i][j] != "DONE":
base = M[i][j]
local_min = min(M[i][j-1],M[i-1][j],M[i-1][j-1])
if base == local_min:
i = i - 1
j = j - 1
array_append(ops_array,i,j)
elif M[i][j-1] < M[i-1][j]:
j = j -1
array_append(ops_array,i,j)
elif M[i-1][j] < M[i][j-1]:
i = i - 1
array_append(ops_array,i,j)
else:
i = i - 1
j = j - 1
array_append(ops_array,i,j)
print ops_array
#########
return M[k-1][l-1]
print med('lead', 'last')
答案 0 :(得分:32)
我认为在这种情况下更深入地理解算法非常重要。我将向您介绍算法的基本步骤,而不是给您一些伪代码,并向您展示您想要的数据是如何被编码的#34;在最终矩阵中产生。当然,如果您不需要推出自己的算法,那么您显然应该使用其他人作为MattH suggests!
这在我看来就像Wagner-Fischer algorithm的实现一样。基本思路是计算&#34;附近和#34;之间的距离。前缀,取最小值,然后计算当前字符串对的距离。例如,假设您有两个字符串'i'
和'h'
。让我们沿着矩阵的垂直和水平轴放置它们,如下所示:
_ h
_ 0 1
i 1 1
这里,'_'
表示一个空字符串,矩阵中的每个单元格对应一个输入(''
或'i'
)到输出的编辑序列({{1 }或''
)。
从空字符串到任何长度为L的字符串的距离为L,(需要L插入)。从任何长度为L的字符串到空字符串的距离也是L(需要L个删除)。这涵盖了第一行和第一列中的值,它们只是递增。
从那里,您可以通过从上左,右和左上角值中取最小值并添加一个来计算任何位置的值,或者,如果字符串中该点的字母相同,取左上角值不变。对于上表中'h'
处的值,(1, 1)
处的最小值为0
,因此(0, 0)
处的值为(1, 1)
,且该值为1
。从'i'
到'h'
的最小编辑距离(一次替换)。因此,通常,最小编辑距离始终位于矩阵的右下角。
现在让我们做另一个,将is
与hi
进行比较。同样,矩阵中的每个单元格对应一个编辑序列,该输入将输入(''
,'i'
或'is'
)输出到输出(''
,{{1 },或'h'
)。
'hi'
我们首先扩展矩阵,使用 _ h i
_ 0 1 2
i 1 1 #
s 2 # #
作为我们尚不了解的值的占位符,并通过递增来扩展第一行和第一列。完成后,我们可以开始计算上面标有#
的位置的结果。让我们从#
开始(在(行,列)中,即行主要表示法)。在上,左上和左值中,最小值为(2, 1)
。表格中的相应字母不同 - 1
和s
- 因此我们将一个最小值添加到h
,然后继续。
2
让我们继续 _ h i
_ 0 1 2
i 1 1 #
s 2 2 #
的值。现在情况有所不同,因为表中相应的字母是相同的 - 它们都是(1, 2)
。这意味着我们可以选择在左上角的单元格中取值而不添加一个。这里的指导性直觉是我们不必增加计数,因为在这个位置上两个字符串都添加了相同的字母。而且由于两根琴弦的长度都增加了一倍,我们会对角移动。
i
对于最后一个空单元格,事情会恢复正常。相应的字母为 _ h i
_ 0 1 2
i 1 1 1
s 2 2 #
和s
,因此我们再次使用最小值并添加一个,以获得i
:
2
如果我们继续这个过程,我会得到两个较长的单词,这些单词以 _ h i
_ 0 1 2
i 1 1 1
s 2 2 2
和is
- hi
开头(忽略标点符号)和{{1 }}:
isnt
这个矩阵稍微复杂一点,但这里的最终最小编辑距离仍然只是hint
,因为这两个字符串的最后两个字母是相同的。方便!
那么我们如何从这个表中提取编辑类型呢?关键是要认识到桌子上的移动对应于特定类型的编辑。例如,从 _ h i n t
_ 0 1 2 3 4
i 1 1 1 2 3
s 2 2 2 2 3
n 3 3 3 2 3
t 4 4 4 3 2
到2
的向右移动将我们从(0, 0)
转移到(0, 1)
,不需要编辑,需要一次编辑,插入。同样,从_ -> _
向_ -> h
的向下移动将我们从(0, 0)
转移到(1, 0)
,不需要编辑,需要一次编辑,删除。最后,从_ -> _
到i -> _
的对角线移动将我们从(0, 0)
转移到(1, 1)
,不需要编辑,需要一次编辑,替换。
所以现在我们所要做的就是颠倒我们的步骤,从上,左和左上角的单元格中追溯局部最小值,回到原点_ -> _
,记住如果当前值是与最小值相同,然后我们必须转到左上角的单元格,因为这是唯一一种不会增加编辑距离的运动。
以下是您可以采取的步骤的详细说明。从完成矩阵的右下角开始,重复以下操作,直至到达左上角:
i -> h
)。在这种情况下不需要编辑,因为此位置的字符是相同的。在上面的示例中,有两种可能的路径:
(0, 0)
和
Equal
扭转它们,我们得到
(4, 4) -> (3, 3) -> (2, 2) -> (1, 2) -> (0, 1) -> (0, 0)
和
(4, 4) -> (3, 3) -> (2, 2) -> (1, 1) -> (0, 0)
因此对于第一个版本,我们的第一个操作是向右移动,即插入。插入的字母为(0, 0) -> (0, 1) -> (1, 2) -> (2, 2) -> (3, 3) -> (4, 4)
,因为我们已从(0, 0) -> (1, 1) -> (2, 2) -> (3, 3) -> (4, 4)
移至h
。 (这对应于详细输出中的isnt
。)我们的下一个操作是对角线移动,即替换或无操作。在这种情况下,它是无操作,因为两个位置的编辑距离相同(即字母相同)。所以hint
。然后向下移动,对应于删除。删除的字母为Insert, h
,因为我们再次从Equal, i, i
移至s
。 (通常,要插入的字母来自输出字符串,而要删除的字母来自输入字符串。)因此isnt
。然后两个对角线移动,值没有变化:hint
和Delete, s
。
结果:
Equal, n, n
在Equal, t, t
上执行这些说明:
Insert, h
Equal, i, i
Delete, s
Equal, n, n
Equal, t, t
总编辑距离为2.
我将第二条最小路径作为练习。请记住,两条路径完全相同;它们可能不同,但它们会产生相同的最小编辑距离2,因此完全可以互换。当您在矩阵中向后工作时,如果您看到两个不同的可能的局部最小值,您可以选择其中一个,并确保最终结果是正确的
一旦你理解了这一切,根本不应该编码。在这种情况下,关键是首先深入理解算法。一旦你完成了这项工作,编码就很容易了。
作为最后一点,您可以在填充矩阵时选择累积编辑。在这种情况下,矩阵中的每个单元格都可以是元组:isnt
。您将增加长度,和附加与最小先前状态的移动相对应的操作。这消除了回溯,因此降低了代码的复杂性;但它占用了额外的内存。如果这样做,最终的编辑序列将与矩阵右下角的最终编辑距离一起出现。
答案 1 :(得分:9)
我建议您查看python-Levenshtein模块。可能会在那里找到很长的路要走:
>>> import Levenshtein
>>> Levenshtein.editops('LEAD','LAST')
[('replace', 1, 1), ('replace', 2, 2), ('replace', 3, 3)]
您可以处理编辑操作的输出以创建详细说明。
答案 2 :(得分:2)
我不知道python,但如果有任何帮助,以下C#代码可以正常工作。
public class EditDistanceCalculator
{
public double SubstitutionCost { get; private set; }
public double DeletionCost { get; private set; }
public double InsertionCost { get; private set; }
public EditDistanceCalculator() : this(1,1, 1)
{
}
public EditDistanceCalculator(double substitutionCost, double insertionCost, double deletionCost)
{
InsertionCost = insertionCost;
DeletionCost = deletionCost;
SubstitutionCost = substitutionCost;
}
public Move[] CalcEditDistance(string s, string t)
{
if (s == null) throw new ArgumentNullException("s");
if (t == null) throw new ArgumentNullException("t");
var distances = new Cell[s.Length + 1, t.Length + 1];
for (int i = 0; i <= s.Length; i++)
distances[i, 0] = new Cell(i, Move.Delete);
for (int j = 0; j <= t.Length; j++)
distances[0, j] = new Cell(j, Move.Insert);
for (int i = 1; i <= s.Length; i++)
for (int j = 1; j <= t.Length; j++)
distances[i, j] = CalcEditDistance(distances, s, t, i, j);
return GetEdit(distances, s.Length, t.Length);
}
private Cell CalcEditDistance(Cell[,] distances, string s, string t, int i, int j)
{
var cell = s[i - 1] == t[j - 1]
? new Cell(distances[i - 1, j - 1].Cost, Move.Match)
: new Cell(SubstitutionCost + distances[i - 1, j - 1].Cost, Move.Substitute);
double deletionCost = DeletionCost + distances[i - 1, j].Cost;
if (deletionCost < cell.Cost)
cell = new Cell(deletionCost, Move.Delete);
double insertionCost = InsertionCost + distances[i, j - 1].Cost;
if (insertionCost < cell.Cost)
cell = new Cell(insertionCost, Move.Insert);
return cell;
}
private static Move[] GetEdit(Cell[,] distances, int i, int j)
{
var moves = new Stack<Move>();
while (i > 0 && j > 0)
{
var move = distances[i, j].Move;
moves.Push(move);
switch (move)
{
case Move.Match:
case Move.Substitute:
i--;
j--;
break;
case Move.Insert:
j--;
break;
case Move.Delete:
i--;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
for (int k = 0; k < i; k++)
moves.Push(Move.Delete);
for (int k = 0; k < j; k++)
moves.Push(Move.Insert);
return moves.ToArray();
}
class Cell
{
public double Cost { get; private set; }
public Move Move { get; private set; }
public Cell(double cost, Move move)
{
Cost = cost;
Move = move;
}
}
}
public enum Move
{
Match,
Substitute,
Insert,
Delete
}
一些测试:
[TestMethod]
public void TestEditDistance()
{
var expected = new[]
{
Move.Delete,
Move.Substitute,
Move.Match,
Move.Match,
Move.Match,
Move.Match,
Move.Match,
Move.Insert,
Move.Substitute,
Move.Match,
Move.Substitute,
Move.Match,
Move.Match,
Move.Match,
Move.Match
};
Assert.IsTrue(expected.SequenceEqual(new EditDistanceCalculator().CalcEditDistance("thou-shalt-not", "you-should-not")));
var calc = new EditDistanceCalculator(3, 1, 1);
var edit = calc.CalcEditDistance("democrat", "republican");
Console.WriteLine(string.Join(",", edit));
Assert.AreEqual(3, edit.Count(m => m == Move.Match)); //eca
}