0 问题介绍
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符(insertion)
- 删除一个字符(deletion)
- 替换一个字符(Substitution)
例如
w1=“kitty”, 2=“puittys”. w1->w2的编辑距离如下
- kitty->pitty,替换k->s
- pitty->puitty ,插入u
3.puitty->puittys,插入s
所以 w 1 w_1 w1-> w 2 w_2 w2的编辑距离为3.
1 数学解答
我们定义两个字符串
w
1
w_1
w1和
w
2
w_2
w2,它们的长度分别是
∣
w
1
∣
,
∣
w
2
∣
|w_1|,|w_2|
∣w1∣,∣w2∣,
w
1
w_1
w1->
w
2
w_2
w2的编辑距离为
E
d
i
s
w
1
,
w
2
(
i
,
j
)
Edis_{w_1,w_2}(i,j)
Edisw1,w2(i,j). 其中i和j分别表示w1和w2中前
i
i
i个和前
j
j
j个字符串截取,那么
w
1
w_1
w1和
w
2
w_2
w2的编辑距离可以用以下数学公式描述:
E
d
i
s
w
1
,
w
2
(
i
,
j
)
=
{
m
a
x
(
i
,
j
)
,
i
f
m
i
n
(
i
,
j
)
=
0
m
i
n
{
E
d
i
s
(
i
−
1
,
j
)
+
1
,
E
d
i
s
(
i
,
j
−
1
)
+
1
E
d
i
s
(
i
−
1
,
j
−
1
)
+
1
,
w
h
i
l
e
w
1
[
i
]
≠
w
2
[
j
]
E
d
i
s
(
i
−
1
,
j
−
1
)
+
0
,
w
h
i
l
e
w
1
[
i
]
=
w
2
[
j
]
,
o
t
h
e
r
w
i
s
e
Edis_{w_1,w_2}(i,j)=\begin{cases} max(i,j) \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \quad \qquad ,if\;min(i,j)=0\\ min\begin{cases} Edis(i-1,j)+1,\\ Edis(i,j-1)+1\\ Edis(i-1,j-1)+1,while \, w_1[i]≠w_2[j] \\ Edis(i-1,j-1)+0, while \, w_1[i]=w_2[j]\\ \end{cases} \quad ,otherwise \end{cases}
Edisw1,w2(i,j)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧max(i,j),ifmin(i,j)=0min⎩⎪⎪⎪⎨⎪⎪⎪⎧Edis(i−1,j)+1,Edis(i,j−1)+1Edis(i−1,j−1)+1,whilew1[i]=w2[j]Edis(i−1,j−1)+0,whilew1[i]=w2[j],otherwise
对于该数学公式描述如下:
1.
E
d
i
s
w
1
,
w
2
(
i
,
j
)
Edis_{w_1,w_2}(i,j)
Edisw1,w2(i,j)表示的是字符串
w
1
w_1
w1和
w
2
w_2
w2中前
i
i
i个字符和前
j
j
j个字符的编辑距离,字符串的下标index从0开始。我们求得最后的编辑距离时
i
=
∣
w
1
∣
,
j
=
∣
w
2
∣
,
E
d
i
s
w
1
,
w
2
(
i
,
j
)
=
E
d
i
s
w
1
,
w
2
(
∣
w
1
∣
,
∣
w
2
∣
)
i=|w1|, j=|w2|,Edis_{w_1,w_2}(i,j)=Edis_{w_1,w_2}(|w_1|,|w_2|)
i=∣w1∣,j=∣w2∣,Edisw1,w2(i,j)=Edisw1,w2(∣w1∣,∣w2∣)
2.当
m
i
n
(
i
,
j
)
=
0
min(i,j)=0
min(i,j)=0时,证明我们截取的
w
1
,
w
2
w1,w2
w1,w2(记作
w
1
′
,
w
2
′
w_1',w_2'
w1′,w2′)存在空串,此时的编辑距离就是
m
a
x
(
i
,
j
)
max(i,j)
max(i,j),也就是插入或删除
w
1
′
w_1'
w1′内的所有内容。
3.当
m
i
n
(
i
,
j
)
≠
0
min(i,j)≠0
min(i,j)=0时,
E
d
i
s
w
1
,
w
2
(
i
,
j
)
Edis_{w_1,w_2}(i,j)
Edisw1,w2(i,j)为以下三个值中的最小值
- E d i s w 1 , w 2 ( i − 1 , j ) + 1 Edis_{w_1,w_2}(i-1,j)+1 Edisw1,w2(i−1,j)+1,删除 w 1 [ i ] w_1[i] w1[i].
- E d i s w 1 , w 2 ( i , j − 1 ) + 1 Edis_{w_1,w_2}(i,j-1)+1 Edisw1,w2(i,j−1)+1,在 w 1 [ i + 1 ] w_1[i+1] w1[i+1]的位置插入 w 2 [ j ] w_2[j] w2[j].
- E d i s w 1 , w 2 ( i − 1 , j − 1 ) + 1 Edis_{w_1,w_2}(i-1,j-1)+1 Edisw1,w2(i−1,j−1)+1,替换 w 1 [ i ] w_1[i] w1[i]为 w 2 [ j ] w_2[j] w2[j].
- E d i s w 1 , w 2 ( i − 1 , j − 1 ) + 0 Edis_{w_1,w_2}(i-1,j-1)+0 Edisw1,w2(i−1,j−1)+0, w 1 [ i ] = = w 2 [ j ] w_1[i]==w_2[j] w1[i]==w2[j].
4.为了控制最后的
E
d
i
s
w
1
,
w
2
(
i
−
1
,
j
−
1
)
+
?
Edis_{w_1,w_2}(i-1,j-1)+\,?
Edisw1,w2(i−1,j−1)+?的变化,增加辅助函数
d
(
w
1
[
i
]
?
=
w
2
[
j
]
)
d(w_1[i]?=w_2[j])
d(w1[i]?=w2[j])
d
=
{
1
,
w
1
[
i
]
≠
w
2
[
j
]
0
,
w
1
[
i
]
=
w
2
[
j
]
d=\begin{cases} 1,w_1[i]≠w_2[j]\\ 0,w_1[i]=w_2[j]\\ \end{cases}
d={1,w1[i]=w2[j]0,w1[i]=w2[j]
通过分析,我们很容易就发现这个算法也是由多个重叠的子问题构成的,所以我们要计划使用递归和动态规划两种算法对该问题进行解决。
#2 实例演示
w
1
=
w_1=
w1=“
a
b
c
abc
abc”,
w
2
w_2
w2="
a
d
c
t
adct
adct"
/ | 空 | 0a | 1b | 2c |
---|---|---|---|---|
空 | ||||
0a | ||||
1d | ||||
2c | ||||
3t |
步骤1:初始化第一行第一列
依照公式
/ | 空 | 0a | 1b | 2c |
---|---|---|---|---|
空 | 0 | 1 | 2 | 3 |
0a | 1 | |||
1d | 2 | |||
2c | 3 | |||
3t | 4 |
步骤2:依照公式完全矩阵
/ | 空 | 0a | 1b | 2c |
---|---|---|---|---|
空 | 0 | 1 | 2 | 3 |
0a | 1 | 0 | 1 | 2 |
1d | 2 | 1 | 1 | 2 |
2c | 3 | 2 | 2 | 1 |
3t | 4 | 3 | 3 | 2 |
3 代码实现(PHP)
3.1 递归实现
class Solution {
/**
* @param String $word1
* @param String $word2
* @return Integer
*/
function minDistance($word1, $word2) {
if (strlen($word1)==0)
return strlen($word2);
else if (strlen($word2)==0)
return strlen($word1);
else if ($word1==$word2)
return 0;
if ($word1[strlen($word1)-1]==$word2[strlen($word2)-1])
$d=0;
else
$d=1;
return min($this->minDistance(substr($word1,0,strlen($word1)-1),$word2)+1,
$this->minDistance($word1,substr($word2,0,strlen($word2)-1))+1,
$this->minDistance(substr($word1,0,strlen($word1)-1),substr($word2,0,strlen($word2)-1))+$d);
}
}
class Solution {
/**
* @param String $word1
* @param String $word2
* @return Integer
*/
function minDistance($word1, $word2) {
$LenOfWord1=strlen($word1);
$LenOfWord2=strlen($word2);
if ($LenOfWord1==0)
return $LenOfWord2;
else if ($LenOfWord2==0)
return $LenOfWord1;
else if ($word1==$word2)
return 0;
if ($word1[$LenOfWord1-1]==$word2[$LenOfWord2-1])
$d=0;
else
$d=1;
return min($this->minDistance(substr($word1,0,$LenOfWord1-1),$word2)+1,
$this->minDistance($word1,substr($word2,0,$LenOfWord2-1))+1,
$this->minDistance(substr($word1,0,$LenOfWord1-1),substr($word2,0,$LenOfWord2-1))+$d);
}
}
结果在leetcode上写完提交,测试用例发现超时了,但是我也没发现题目哪里要求了时间。把strlen的结果存储起来,收效甚微。没办法,只好尝试使用动态规划。
3.2 动态规划
时间复杂度:O(mn),空间复杂度O(mn)
class Solution {
/**
* @param String $word1
* @param String $word2
* @return Integer
*/
function minDistance($word1, $word2) {
//获取数组长度
$LenOfWord1=strlen($word1);
$LenOfWord2=strlen($word2);
//新建数组
$arr=array();
//初始化第一行第一列
$temp=0;
for($i=0;$i<$LenOfWord1+1;$i++)
$arr[$i][0]=$temp++;
$temp=0;
for($j=0;$j<$LenOfWord2+1;$j++)
$arr[0][$j]=$temp++;
//完善矩阵
for($i=1;$i<$LenOfWord1+1;$i++){
for($j=1;$j<$LenOfWord2+1;$j++){
if($word1[$i-1]==$word2[$j-1])
$d=0;
else
$d=1;
$arr[$i][$j]=min($arr[$i-1][$j]+1,$arr[$i][$j-1]+1,$arr[$i-1][$j-1]+$d);
}
}
return $arr[$LenOfWord1][$LenOfWord2];
}
}
虽然说执行的速度很快,但是内存消耗太大,不是一个速度和内存综合性得分高的方案,接下来要对其进行一定的优化。
3.3 二维数组一维化
单纯的把上面提到的二维数组直接一维化,对于内存和速度的提升有限,因此我们要探究一种更加优秀的方案。
通过反复书写运算过程,我们可以发现,在初始化二维数组的时候可以只初始化第一行和第二行的第0位。因为在填写第二行的时候,仅仅需要第一行和第二行第一位的数字。在以后的过程中,我们的所有运算也仅仅涉及相邻的两行。因此我们可以通过两个一维数组就可以完成编辑距离的计算。
步骤1:初始化
/ | 0 | 1a | 2b | 3c | 存储该行的数组 |
---|---|---|---|---|---|
0 | 0 | 1 | 2 | 3 | arr0 |
1a | 1 | arr1 | |||
2d | |||||
3c | |||||
4t |
步骤2:填写第二行,同时初始化第三行
/ | 0 | 1a | 2b | 3c | 存储该行的数组 |
---|---|---|---|---|---|
0 | ∅ | 1 | 2 | 3 | arr0[1~3] |
1a | 1 | 1 | 2 | 3 | arr1 |
2d | 2 | arr0[0] | |||
3c | |||||
4t |
第arr0[0]因为被用来初始化(0,2),所以变空,我们的两个数组是循环使用的。下同。
步骤3:以此类推,直到填写到最后一行
/ | 0 | 1a | 2b | 3c | 存储该行的数组 |
---|---|---|---|---|---|
0 | |||||
1a | 1 | 1 | 2 | 3 | arr1 |
2d | 2 | 2 | 2 | 3 | arr0 |
3c | |||||
4t |
……
时间复杂度:O(mn),空间复杂度:O(n)
class Solution {
/**
* @param String $word1
* @param String $word2
* @return Integer
*/
function minDistance($word1, $word2) {
//获取数组长度
$LenOfWord1=strlen($word1);
$LenOfWord2=strlen($word2);
//新建数组
$arr0=array();
$arr1=array();
$count=1;
//初始化第一行第一列
for($i=0;$i<=$LenOfWord1;$i++)
$arr0[$i]=$i;
$arr1[0]=$count;
//完善矩阵
for($count=1;$count<=$LenOfWord2;$count++){
for($i=1;$i<=$LenOfWord1;$i++){
if($word1[$i-1]==$word2[$count-1])
$d=0;
else
$d=1;
if($count%2)
$arr1[$i]=min($arr0[$i]+1,$arr1[$i-1]+1,$arr0[$i-1]+$d);
else
$arr0[$i]=min($arr1[$i]+1,$arr0[$i-1]+1,$arr1[$i-1]+$d);
}
if($count%2){
$arr0[0]=$count+1;}
else{
$arr1[0]=$count+1;
}
}
$count--; //最后一轮for循环多加了一次,因此要在这里减去
if($count%2)
return $arr1[$LenOfWord1];
else
return $arr0[$LenOfWord1];
}
}
值得注意的几点如下
- $count的命名空间一定要覆盖整个函数,切不可在某个循环内.
- 因为for循环的特性,最后以此会是count多被+1一次,为了更符合逻辑,要把count-1;当然如果你脑子拐得过来弯,也可以直接输出,反正我是拐不过来。
经过检查,这是一份优秀的代码