C++ 计算字符串的相似度 (编辑距离)的回溯算法 与 动态规划算法

本文深入解析编辑距离概念,包括莱文斯坦距离与最长公共子串长度的计算方式,通过回溯算法与动态规划两种方法详细阐述如何求解两个字符串间的最小编辑距离。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

编辑距离

        编辑距离指的就是,将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。对于两个完全相同的字符串来说,编辑距离就是 0。

       根据所包含的编辑操作种类的不同,编辑距离有多种不同的计算方式,比较著名的有莱文斯坦距离(Levenshtein distance)最长公共子串长度(Longest common substring length)。其中,莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,最长公共子串长度只允许增加、删除字符这两个编辑操作。

例子:

将单词“kitten”修改为“sitting”最少需要3次单字符的操作:

  1. kitten -> sitten(将“k”改为“s”)
  2. sitten -> sittin(将“e”改为“i”)
  3. 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
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值