【算法题解】计算字符串的编辑距离

291 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情

描述

Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。编辑距离的算法是首先由俄国科学家 Levenshtein 提出的,故又叫 Levenshtein Distance 。

例如:

字符串A: abcdefg

字符串B: abcdef

通过增加或是删掉字符 ”g” 的方式达到目的。这两种方案都需要一次操作。把这个操作所需要的次数定义为两个字符串的距离。

要求:

给定任意两个字符串,写出一个算法计算它们的编辑距离。

数据范围:给定的字符串长度满足 1≤len(str)≤1000 

输入描述:

每组用例一共2行,为输入的两个字符串

输出描述:

每组用例输出一行,代表字符串的距离

示例1

输入:

abcdefg
abcdef

输出:

1

题目的主要信息:

  • Levenshtein 距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数,编辑可包括将一个字符替换成另一个字符,插入一个字符,删除一个字符
  • 给定任意两个字符串,计算二者的编辑距离

方法一:动态规划

具体做法:

我们用二维dp数组记录编辑距离,其中dp[i][j]表示字符串s1前i个字符与字符串s2前j个字符之间的编辑距离。

初始状态时,s1中第0个字符与s2中每个字符的距离就是s2每个字符为止的长度,s2中第0个字符与s1中每个字符的距离就是s1每个字符为止的长度,因此二维数组第1行第1列分别置为相应的列号和行号。

后面我们对于字符串s1中从1到n每个长度,都计算相应的到字符串2从1到m每个长度的距离。转移方程为:

dp[i][j]=min{dp[i−1][j−1]+(s1[i]==s2[j]?0:1),min(dp[i−1][j],dp[i][j−1]+1)}

其中dp[i−1][j]表示字符串s1前i−1个字符到字符串s2前j个字符的距离,增加一个字符即可得到,dp[i][j−1]表示字符串s1前i个字符到字符串s2前j−1个字符的距离, 删除一个字符即可得到,因此转移都是加1次操作。dp[i−1][j−1]表示字符串s1前i−1个字符到字符串s2前j−1个字符的距离,如果现在两个字符相同,则不变,否则增加一次修改操作,距离加1.

#include<bits/stdc++.h>
using namespace std;

int main(){
    string s1, s2;
    while(cin >> s1 >> s2){
        int n = s1.length();
        int m = s2.length();
        vector<vector<int> > dp(n + 1, vector<int>(m + 1, 0)); //dp[i][j]表示s1中前i个字符与s2中前j个字符的距离
        for(int i = 1; i <= m; i++)  //s1中第0个字符与s2中每个字符的距离就是s2每个字符为止的长度
            dp[0][i] = i;
        for(int i = 1; i <= n; i++)  //s2中第0个字符与s1中每个字符的距离就是s1每个字符为止的长度
            dp[i][0] = i;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                int temp = dp[i - 1][j - 1];
                if(s1[i - 1] != s2[j - 1]) //如果前两个字符不同,要多增加1的距离
                    temp++;
                dp[i][j] = min(temp, min(dp[i - 1][j], dp[i][j - 1]) + 1); //状态转移,选出最小
            }
        }
        cout << dp[n][m] << endl;
    }
    return 0;
}

复杂度分析:

  • 时间复杂度:O(nm)O(nm),其中n和m分别为两个字符串的长度,两层遍历
  • 空间复杂度:O(nm)O(nm),辅助数组dp的大小

方法二:动态规划空间优化

具体做法:

我们发现方法一在遍历的时候二维数组只使用到了本行i及上一行i−1,因为我们可以使用滚动数组优化(k=1−k,k表示行),每次遍历都只有两行,而下标在不断交替使用。每行首列在外循环进入的时候初始化为行号,原理同上。

#include<bits/stdc++.h>
using namespace std;

int main(){
    string s1, s2;
    while(cin >> s1 >> s2){
        int n = s1.length();
        int m = s2.length();
        vector<vector<int> > dp(2, vector<int>(m + 1, 0)); //dp[i][j]表示s1中前i个字符与s2中前j个字符的距离,滚动数组表示
        for(int i = 1; i <= m; i++)  //s1中第0个字符与s2中每个字符的距离就是s2每个字符为止的长度
            dp[0][i] = i;
        int k = 0;
        for(int i = 1; i <= n; i++){
            k = 1 - k; //数组交换行
            dp[k][0] = i; //初始置为该行号
            for(int j = 1; j <= m; j++){
                int temp = dp[1 - k][j - 1];
                if(s1[i - 1] != s2[j - 1]) //如果这两个字符不同,要多增加1的距离
                    temp++;
                dp[k][j] = min(temp, min(dp[1 - k][j], dp[k][j - 1]) + 1); //状态转移,选出最小
            }
        }
        cout << dp[k][m] << endl;
    }
    return 0;
}

复杂度分析:

  • 时间复杂度:O(nm)O(nm),其中n和m分别为两个字符串的长度,两层遍历
  • 空间复杂度:O(m)O(m),辅助数组dp只有两行,每行长度m+1