编辑距离
编辑距离指的就是,将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。对于两个完全相同的字符串来说,编辑距离就是 0。
根据所包含的编辑操作种类的不同,编辑距离有多种不同的计算方式,比较著名的有莱文斯坦距离(Levenshtein distance)和最长公共子串长度(Longest common substring length)。其中,莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,最长公共子串长度只允许增加、删除字符这两个编辑操作。
例子:
将单词“kitten”修改为“sitting”最少需要3次单字符的操作:
- kitten -> sitten(将“k”改为“s”)
- sitten -> sittin(将“e”改为“i”)
- sittin -> sitting(将“g”删除)
原理:
假设现在两个字符串A和B,其中A的长度为a,B的长度为b,现要计算A与B之间的 Levenshtein distance
我们可以考虑使用回溯算法与动态规划的思想解决这个问题
回溯算法解决:
回溯算法在过程中就是不断遍历、回溯各个决策路径、更新最大值的问题。过程中由于使用 增加、删除、替换字符 三种方式来进行处理,加上字符相等这个情况,一共是4中情况,对于也就是不断在决策过程中遍历这些处理方式,比较好理解,可以通过绘制决策树的形式来理解递归的过程,具体代码如下:
//
// Created by BigHuang on 2020/9/10.
//
#include <iostream>
#include <string>
using namespace std;
string a = "mitamu";
string b = "mtacnu";
int n = a.length();
int m = b.length();
int min_dist = INT_MAX;
void lwstBT(int i, int j, int edist) {
if (i == n || j == m) {
if (i < n) edist += (n-i); // a长,直接加上后续多余长度
if (j < m) edist += (m-j); // 同上
if (edist < min_dist) min_dist = edist; // 更新最大值
return;
}
if (a[i] == b[j]) { // 两个字符匹配
lwstBT(i+1, j+1, edist);
} else { // 两个字符不匹配
lwstBT(i+1, j, edist+1); // 删除a[i]或者b[j]前添加一个字符
lwstBT(i, j+1, edist+1); // 删除b[j]或者a[i]前添加一个字符
lwstBT(i+1, j+1, edist+1); // 将a[i]和b[j]替换为相同字符
}
}
int main() {
lwstBT(0, 0, 0);
cout << "min_dist = " << min_dist << endl;
return 0;
}
动态规划算法解决:
回溯是一个递归处理的过程。如果 a[i]与 b[j]匹配,我们递归考察 a[i+1]和 b[j+1]。如果 a[i]与 b[j]不匹配,那我们有多种处理方式可选:
- 可以删除 a[i],然后递归考察 a[i+1]和 b[j];
- 可以删除 b[j],然后递归考察 a[i]和 b[j+1];
- 可以在 a[i]前面添加一个跟 b[j]相同的字符,然后递归考察 a[i]和 b[j+1];
- 可以在 b[j]前面添加一个跟 a[i]相同的字符,然后递归考察 a[i+1]和 b[j];
- 可以将 a[i]替换成 b[j],或者将 b[j]替换成 a[i],然后递归考察 a[i+1]和 b[j+1]。
从上述递归需要考察的结果来看,问题之间以合并的,最终也就三种情况,但是考虑到如果字符串相等这一情况,可以把问题分解,最后可以得到状态转移方程如下:
上述状态转移方程中,这里面我开始思考的时候存在误区,认为最后字符串比较的部分不应该比较 ai 与 bj ,我认为应该比较a i-1 与 b j-1 。其实这个想法由来是片面的,我使用最稳妥的回溯算法求解的结果,验证了这个想法的失败,之后反复思考过程中发现,本质为没有对这个问题有个很好的认识。求解 (i, j) 这一状态表时,应该理解为 (i, j) 应该可以由哪些子问题得到,最终取其最优解,也就是上述方程中 otherwise 的部分,方程中此时lev a,b()的部分都表示前一状态的结果,两个状态之间相差什么呢? 为这一状态的结果!例如 部分,其实已经考虑到了 a i-1 与 b j-1 的结果,我们现在需要添加的部分应该是 a i 与 b j 的比较结果。所以针对 a i 与 b j,如果从转移表上边或者左边来,直接加一就好
,但是如果从对角线来,那就是需要考虑这个 a i 与 b j 是否相等了,不相等就在 a i-1 与 b j-1 基础上加 1 ,相同就顺延,这样看就好理解了。代码如下:
//
// Created by BigHuang on 2020/9/10.
//
#include <iostream>
#include <string>
using namespace std;
string a = "abdhcdeh";
string b = "ahcehjkfldk";
int n = a.length();
int m = b.length();
int min_dist = INT_MAX;
void lwst(){
int** arr = new int* [n];
for (int i = 0; i < n; ++i)
arr[i] = new int [m];
// 初始化第一列
for (int i = 0; i < n; ++i) {
if (b[0] == a[i]) arr[i][0] = i;
else {
if (i > 0)
arr[i][0] = arr[i-1][0] + 1;
else
arr[i][0] = 1;
}
}
// 初始化第一行
for (int j = 0; j < m; ++j) {
if (a[0] == b[j]) arr[0][j] = j;
else{
if (j > 0)
arr[0][j] = arr[0][j-1] + 1;
else
arr[0][j] = 1;
}
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
if (a[i] == b[j]) {
arr[i][j] = min(arr[i-1][j]+1, min(arr[i][j-1]+1, arr[i-1][j-1]));
} else {
arr[i][j] = min(arr[i-1][j]+1, min(arr[i][j-1]+1, arr[i-1][j-1]+1));
}
}
}
cout << "min distence = " << arr[n-1][m-1];
}
int main() {
lwst();
return 0;
}
顺便贴一下动态规划做的 最长公共子串长度的求解:
理解了上述动态规划,这个是类似的。
//
// Created by BigHuang on 2020/9/10.
//
#include <iostream>
#include <string>
using namespace std;
string a = "mitamu";
string b = "mtacnu";
int n = a.length();
int m = b.length();
int long_sub = INT_MIN;
void lcs() {
//新建堆区数据
int** arr = new int* [n];
for (int i = 0; i < n; ++i)
arr[i] = new int [m];
// 初始化第一列
for (int i = 0; i < n; ++i) {
if (a[i] == b[0]) arr[i][0] = 1;
else {
if (i != 0)
arr[i][0] = arr[i-1][0];
else
arr[i][0] = 0;
}
}
// 初始化第一列
for (int j = 0; j < m; ++j) {
if (a[0] == b[j]) arr[0][j] = 1;
else {
if (j != 0)
arr[0][j] = arr[0][j-1];
else
arr[0][j] = 0;
}
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
if (a[i] == b[i])
arr[i][j] = max(max(arr[i][j-1], arr[i-1][j]), arr[i-1][j-1]+1);
else
arr[i][j] = max(max(arr[i][j-1], arr[i-1][j]), arr[i-1][j-1]);
}
}
cout << "longest distence = " << arr[n-1][m-1];
}
int main() {
lcs();
return 0;
}
最后,感谢前辈对于一些地方做的详细介绍: https://www.jianshu.com/p/492536dfa98f