引入

编辑距离(Edit Distance),又称\(Levenshtein\)距离,是指两个字串之间,由一个转成另一个所需的编辑操作次数。最小编辑距离,是指所需最小的编辑操作次数。

编辑操作包含:插入、删除和替换三种操作。
插入:在某个位置插入一个字符
删除:删除某个位置的字符
替换:把某个位置的字符换成另一个字符

经典做法:动态规划

这种类型的题目与\(LCS\)的做法有异曲同工之妙。设\(dp[i][j]\)表示第一个字符串\(str1\)前\(i\)位与第二个字符串\(str2\)前\(j\)位进行匹配所需的最小编辑距离。
考虑\(i、j\)处的状态转移,假设第一个字符串为目标串,有以下三种情况:

  1. 由\(i-1、j\)转移而来,即是执行插入操作,在\(str2\)的第\(j\)位加入\(str1[i]\)字符。
  2. 由\(i、j-1\)转移而来,即是执行删除操作,删除\(str2[j]\)。
  3. 由\(i-1、j-1\)转移而来,若两者不同,则执行替换操作,否则不做处理。

故转移方程为

\[dp[i][j]=min(dp[i-1][j]+1,min(dp[i][j-1]+1,dp[i-1][j-1]+flag))(flag=0/1) \]

时间复杂度:\(O(NM)\)。空间复杂度:\(O(NM)\)。
可以采用类似\(LCS\)的优化方法优化时空。
\(LCS\)优化方法:

变式1:只有插入与删除操作

在这种情况下的答案\(ans\)满足:

\[ans = len_n + len_m - 2*lcs(str1,str2) \]

形象理解就是在去除掉两者的\(LCS\)之后将\(str2\)清空,然后执行插入操作。

变式2:同样只有插入与删除操作,但存在最小编辑次数限制

当最小编辑次数超过\(K\)时,输出\(-1\);否则输出最小编辑次数。
数据范围:\(len_n,len_m<=501000,K<=100\)
如果采用常规算法,时空都会超限。
容易观测到\(K\)的值很小,主观感受\(K\)应是本题的关键,猜测时间复杂度应为\(O(len*K)\)。

观测原本的状态转移的限制:需要进行两重循环,计算每一个\(i\)与\(j\)对应的状态。

关键在于:离\(i\)的距离大于\(K\)的\(j\)对应的状态是无用的,因为无论如何都至少需要匹配str1的前\(i\)位,如果从这些\(j\)进行转移,修改次数就会大于\(K\)。

我们可以利用这一点设计状态,优化时空。

设\(dp[i][j]\)表示匹配\(str1\)前\(i\)位与\(str2\)前\(i+j-K\)位所需要的最小修改次数,第二维即代表两个串在该阶段匹配相差的长度,相差长度为\(abs(j-K)\)。

为方便起见,将\(j\)的值整体加\(K\),方便储存数组以及进行计算,以免出现负数。

当\(j<K\),代表匹配的\(str2\)长度比\(str1\)短

转移的情况可能为:

  1. \(str1[i]==str2[i+j] -> dp[i][j]=dp[i-1][j]\)(上下同时取一个字符,相对距离不变)
  2. \(str1[i]!=str2[i+1]->dp[i][j]=min(dp[i-1][j+1]+1,dp[i][j-1]+1)\)

解释:\(dp[i-1][j+1]+1\)代表着添加操作,即是在\(str2[i+j]\)的后面添加\(str1[i]\),因此需要让\(str1[i-1]\)与\(str2[i+j-K]\)做好匹配并以此进行转移,这一个阶段相对长度对应的值为\(j+1\),故为\(dp[i-1][j+1]\)。
(注意:当\(j\)小于K时,\(j+1\)代表相对距离变小,大于则代表变大)
\(dp[i][j-1]\)同理。

初始时其实应该把\(str2\)匹配段比\(str1\)匹配段短与长的两种情况分开考虑,此时添加时相对长度的变化也有所不同,但若使用将相差长度加\(k\)作为第二维,在转移时相对长度的变化对应的操作就一致了。

初始化:假设目标串长度为0,则原始串只进行删除操作。

\[dp[0][i]=i-k \]

(注意此处的\(i\)大于\(k\),代表原始串比目标串长)

总体时间复杂度为\(O(len*K)\),空间复杂度为\(O(len*K)\)。

注意:还可以利用滚动数组,使用类似与\(LCS\)的空间优化手段。

若序列的元素重复度不高,还可以利用时间优化手段求\(LCS\)解决此题。

void solve(char *str1, char *str2)
{
    for(int i = k; i <= 2 * k; i++) f[0][i] =  i - k;
    for(int i = 0; i <= 2 * k + 1; i++) f[1][i] = maxn;
    for(int i = 0; i < len_n; i++)
    {
        int left_pos = max(0, i - k);
        int right_pos = min(len_m - 1, i + k);
        for( int j = left_pos; j <= right_pos; j++ )
        {
            int now_dis = j - i + k;
            if(str1[j] == str2[i])
            {
                f[1][now_dis] = f[0][now_dis]; //直接继承,长度差不变
            }   
            else 
            {
                if(now_dis == 0) f[1][now_dis] = min(f[1][now_dis], f[0][now_dis + 1] + 1);
                else f[1][now_dis] = min(f[1][now_dis], min(f[0][now_dis + 1],f[1][now_dis - 1]) + 1);
            }         
            //注意到 f[0][now_dis+1]+1与f[1][now_dis-1]+1永远都是大于等于f[0][now_dis]
        }
        for( int j = 0; j <= 2 * k; j++ )
        {
            f[0][j] = f[1][j];
            f[1][j] = maxn;
        }
    }
}