字符串处理-DP问题

引用:
labuladong
liweiwei
图片来自weiwei大神
在这里插入图片描述

72.编辑距离 字符串匹配

全文引用Labuladong
在这里插入图片描述

1.思路

前文「最长公共子序列」说过,解决两个字符串的动态规划问题,一般都是用两个指针 i,j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。

设两个字符串分别为 “rad” 和 “apple”,为了把 s1 变成 s2,算法会这样进行:

当s1[i]==a2[j]时,可以skip
当i!=j时,可以insert/replace/delete
还有一个很容易处理的情况:
就是 j 走完 s2 时,如果 i 还没走完 s1,那么只能用删除操作把 s1 缩
短为 s2
类似的,如果 i 走完 s1 时 j 还没走完了 s2,那就只能用插入操作把 s2
剩下的字符全部插入 s1。等会会看到,这两种情况就是算法的 base
case(类似于归并排序)

2.代码详解

先梳理一下之前的思路:
base case 是 i 走完 s1 或 j 走完 s2,可以直接返回另一个字符串剩下的长度。
对于每对儿字符 s1[i] 和 s2[j],可以有四种操作:

if s1[i] == s2[j]:
    啥都别做(skip)
    i, j 同时向前移动
else:
    三选一:
        插入(insert)
        删除(delete)
        替换(replace)

在这里插入图片描述

有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,理解需要点技巧,先看下代码:

def minDistance(s1, s2) -> int:

    def dp(i, j):
        # base case
        if i == -1: return j + 1 # 一开始s1没有长度
        if j == -1: return i + 1

        if s1[i] == s2[j]:
            return dp(i - 1, j - 1)  # 啥都不做
        else:
            return min(
                dp(i, j - 1) + 1,    # 插入
                dp(i - 1, j) + 1,    # 删除
                dp(i - 1, j - 1) + 1 # 替换
            )

    # i,j 初始化指向最后一个索引
    return dp(len(s1) - 1, len(s2) - 1)

下面来详细解释一下这段递归代码,base case 应该不用解释了,主要解释一下递归部分。
都说递归代码的可解释性很好,这是有道理的,只要理解函数的定义,就能很清楚地理解算法的逻辑。我们这里 dp(i, j) 函数的定义是这样的:

def dp(i, j) -> int
# 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离

关于#替换/插入/删除/什么都不做/的gif图见Labuladong

现在,你应该完全理解这段短小精悍的代码了。还有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。
怎么能一眼看出存在重叠子问题呢?前文「动态规划之正则表达式」有提过,这里再简单提一下,需要抽象出本文算法的递归框架:

def dp(i, j):
    dp(i - 1, j - 1) #1
    dp(i, j - 1)     #2
    dp(i - 1, j)     #3

对于子问题 dp(i-1, j-1),如何通过原问题 dp(i, j) 得到呢?有不止一条路径,比如 dp(i, j) -> #1 和 dp(i, j) -> #2 -> #3。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。

3.动态规划优化,暴搜+备忘录解法

对于重叠子问题呢,前文「动态规划详解」详细介绍过,优化方法无非是备忘录或者 DP table。
备忘录很好加,原来的代码稍加修改即可:
暴搜+备忘录解法很实用

def minDistance(s1, s2) -> int:

    memo = dict() # 备忘录
    def dp(i, j):
        if (i, j) in memo: 
            return memo[(i, j)]
        ...

        if s1[i] == s2[j]:
            memo[(i, j)] = ...  
        else:
            memo[(i, j)] = ...
        return memo[(i, j)]

    return dp(len(s1) - 1, len(s2) - 1)
#include <iostream>
#include <string>
#include <vector>
#include <limits.h>

using namespace std;
    
int dfs(int i, int j, vector<vector<int>>& memo, string s1, string s2){
   
   
    //有一个已经到头了(此处已经包括处理了两个都到头的情况)
    //dp(i, j)的含义:s1[0,i)左闭右开与s2[0,j)左闭右开, 最少操作数
    if(i==-1) return j+1; // 当i=-1时即i指向队头监视,返回s2剩余的长度:j-(-1)
    if(j==-1) return i+1; 
    if(memo[i][j]>0) return memo[i][j];
    if(s1[i]==s2[j]){
   
   
        memo[i][j]=dfs(i-1,j-1, memo, s1, s2);
    }
    else{
   
   
        
        int operator1 = dfs(i-1,j, memo, s1, s2)+1;
        int operator2 = dfs(i,j-1, memo, s1, s2)+1;
        int operator3 = dfs(i-1,j-1, memo, s1, s2)+1;
        memo[i][j]=min(operator1, min(operator2, operator3));
    }
    return memo[i][j];
}
    


int main(){
   
   
    
    string s1;
    string s2;
    while(cin>>s1>>s2){
   
   
        // s1->s2
        //if(s1.length()==0) return s2.length(); // 加上word2的所有字符
        //if(s2.length()==0) return s1.length(); // 删掉word2的所有字符
        
        vector<vector<int>> memo(s1.length(), vector<int>(s2.length()));
        cout << dfs(s1.length()-1, s2.length()-1,memo, s1, s2) << endl;
    }
    return 0;
}
4.DP格子方法

首先明确 dp 数组的含义,dp 数组是一个二维数组,长这样:
在这里插入图片描述
有了之前递归解法的铺垫,应该很容易理解。dp[..][0]dp[0][..]对应 base case,dp[i][j](数组,动态规划自底向上)的含义和之前的 dp 函数(自顶向上,递归函数)类似:

def dp(i, j) -> int
// 返回 s1[0…i] 和 s2[0…j] 的最小编辑距离
dp[i-1][j-1]
// 存储 s1[0…i] 和 s2[0…j] 的最小编辑距离

dp 函数的 base case 是 i,j 等于 -1,而数组索引至少是 0,所以 dp 数组会偏移一位。
既然 dp 数组和递归 dp 函数含义一样,也就可以直接套用之前的思路写代码,唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解:

int minDistance(string s1, string s2){
   
   
	int m = s1.length(), n = s2.length();
	vector<vector<int>> dp(m+1, vector<int>(n+1));
	// base case
	for(int i =1; i<=m; i++){
   
   
		dp[i][0]=i;
	}
	for(int j =1; j<=n; j
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值