首页 > 编程语言 >算法学习笔记(8.6)-编辑距离问题

算法学习笔记(8.6)-编辑距离问题

时间:2024-07-14 21:30:06浏览次数:18  
标签:字符 8.6 range 笔记 编辑 算法 字符串 步数 dp

目录

Question:

动态规划思路:

第一步:思考每轮的决策,定义状态,从而得到dp表

第二步:找出最优子结构,进而推导出状态转移方程

第三步:确定边界条件和状态转移顺序

代码实现:

图例:

空间优化:

代码如下

编辑距离,也称为Levenshtein距离,指两个字符串之间互相转化的最少修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。

Question

输入两个字符串s和t,返回将s转化为t所需的最少编辑步数。

你可以在一个字符串中进行三种编辑操作:插入一个字符、删除一个字符、将字符替换成任意字符

如图所示,将kitten转化为sitting需要编辑3步,包括两次替换操作与一次添加操作;将hello转化为algo需要三步,包括两次替换操作和一次删除操作。

编辑距离问题可以很自然地利用决策树模型来解释。字符串对应树节点,一轮决策(一次编辑操作)对应树上的一条边。

如图所示,在不限制操作的情况下,每个节点都可以派生出许多条边,每条边对应一种操作,这意味着从hello转化到algo有许多种可能的途径。

从决策树的角度看,本题的目标是求解节点hello和节点之间的最短距离。

动态规划思路:

第一步:思考每轮的决策,定义状态,从而得到dp表

每一轮的决策是对字符串s进行一次编辑操作。

我们希望在编辑操作的过程中,问题的规模逐渐缩小,这样才能构建子问题。设字符串s和t的长度分别为n和m,我们先考虑两字符串尾部的字符s[n-1]和t[m-1]。

  1. 若s[n-1]和t[m-1]相同,我们可以跳过它们,直接考虑s[n-2]和t[m-2]。
  2. 若s[n-1]和t[m-1]不同,我们需要对s进行一次编辑(删除、插入、替换),使得两字符串尾部的字符相同,从而可以跳过它们,考虑规模更小的问题。

也就是说,我们在字符串s中进行的每一轮决策(编辑操作),都会使得s和t中剩余的待匹配字符发生变化。因此,状态为当前在s和t中考虑的第i和第j个字符,记为[i,j]。

状态[i,j]对应的子问题:将s的前i个字符更改为t的前j个字符所需的最少编辑步数。

至此,得到一个尺寸为(i+1)*(j+1)的二维dp表。

第二步:找出最优子结构,进而推导出状态转移方程

考虑子问题dp[i,j],对应的两个字符串的尾部字符为s[i-1]和t[j-1],可根据不同编辑操作分为图例的三种情况。

  1. 在s[i-1]之后添加t[j-1],剩余子问题dp[i,j-1]
  2. 删除s[i-1],剩余子问题dp[i-1,j]。
  3. 将s[i-1]替换为t[j-1],剩余子问题dp[i-1,j-1]。

根据以上分析,可得最优子结构:dp[i,j]的最少编辑步数等于dp[i,j-1]、dp[i-1,j]、dp[i-1,j-1]三者中的最少编辑步数,再加上本次编辑步数为1.对应的状态转移方程为:

dp[i,j] = min(dp[i,j-1],dp[i-1,j],dp[i-1,j-1]) + 1

请注意当s[i-1]和t[j-1]相同时,无须编辑当前字符,这种情况下的状态转移方程为:

dp[i,j] = dp[i-1,j-1]

第三步:确定边界条件和状态转移顺序

当两个字符串都为空时,编辑步数为0,即dp[0,0] = 0。当s为空但t不为空时,最少编辑步数等于t的长度,即首行的dp[0,j] = j。当s不为空但t为空时,最少编辑步数等于s的长度,即首列dp[i,0] = i。

代码实现:
# python 代码示例

def edit_distance_dp(s, t) :
    n, m = len(s), len(t)
    
    dp = [ [0] * (m + 1) for _ in range(n + 1)]
    
    for j in range(1, m + 1) :
        dp[0][j] = j ;
    
    for i in range(1, n + 1) :
        dp[i][0] = i ;
    
    for i in range(1, n + 1) :
        for j in range(1, m + 1) :
            if s[i - 1] == t[j - 1] :
                dp[i][j] = dp[i - 1][j - 1]
            else :
                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
    return dp[n][m]
// c++ 代码示例
int editDistanceDP(string s, string t)
{
    int n = s.length(), m = t.length() ;
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0)) ;
    for (int i = 1 ; i <= n ; i++)
    {
        dp[i][0] = i ;
    }
    for (int j = 1 ; j <= m ; j++)
    {
        dp[0][j] = j ;
    }
    for (int i = 1 ; i <= n ; i++)
    {
        for (int j = 1 ; j <= m ; j++)
        {
            if (s[i - 1] == t[j - 1])
            {
                dp[i][j] = dp[i - 1][j - 1] ;
            }
            else
            {
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1]j - 1]) + 1 ;
            }
        }
    }
    return dp[n][m] ;
}
图例:

空间优化:

由于dp[i,j]是由上方的dp[i-1,j]、左方dp[i,j-1]、左上方dp[i-1,j-1]转移而来的,而正序遍历会丢失左上方dp[i-1,j-1],倒叙遍历无法提前构建dp[i,j-1],因此两种遍历顺序都不可取。

为此,我们可以使用一个变量leftup来暂存左上方的解dp[i-1,j-1],从而只需要考虑作坊和上方的解。此时的情况与完全背包问题相同,可以使用正序遍历。

代码如下:
# python 代码示例

def edit_distance_dp_comp(s, t) :
    n, m = len(s), len(t)
    dp = [0] * (m + 1)
    for j in range(1, m + 1) :
        dp[j] = j 
    for i in range(1, n + 1) :
        leftup = dp[0] # 暂存dp[i - 1][j - 1]
        dp[0] += 1
        for j in range(1, m + 1) :
            temp = dp[j]
            if s[i - 1] == t[j - 1] :
                dp[j] = leftup
            else :
                dp[j] = min(dp[j - 1], dp[j], leftup) + 1
        leftup = temp
    return dp[m]
// c++ 代码示例
int editDistanceDPComp(string s, string t)
{
    int n = s.length(), m = t.length() ;
    vector<int> dp(m + 1, 0) ;
    for (int j = 1 ; j <= m ; j++)
    {
        dp[j] = j ;
    }
    for (int i = 1 ; i <= n ; i++)
    {
        leftup = dp[0] ;
        dp[0]++ ;
        for (int j = 1 ; j <= m ; j++)
        {
            int temp = dp[j] ;
            if (s[i - 1] == t[j - 1])
            {
                dp[j] =leftup ;
            }
            else
            {
                dp[j] = min(dp[j], dp[j - 1], leftup) + 1 ;
            }
            leftup = temp ;
        }
    }
    return dp[m] ;
}

标签:字符,8.6,range,笔记,编辑,算法,字符串,步数,dp
From: https://blog.csdn.net/2301_76874968/article/details/140417305

相关文章

  • 算法学习笔记(8.5)-零钱兑换问题二
    目录Question:动态规划思路:代码实现:空间优化代码Question:给定n种硬币,第i种硬币的面值为coins[i-1],目标金额为amt,每种硬币可以重复选取,问凑出目标金额的硬币组合数量。动态规划思路:相比与上一题,本体的目标是求组合数量,因此子问题变为:前i种硬币能够凑出金额a的组合数......
  • (网络流)最大流-增广路算法
    最大流概念(一般形式、一般模型):在一张有向图中,给定源点、汇点,每条边单位时间可以流x容量的水。有无限的水从源点流入,从汇点流出。求单位时间内,从汇点流出的水的最大值。(网络流)最大流-增广路算法核心思路:每次找到一条可以流水的路径,将其称为增广路。增广路的所以算法本质上都......
  • ROS学习笔记总结篇(基础篇梳理)
    在学习完一到十章节的ros后,我们的ros基础篇也迎来了结束,因此,在这章节,我会做一个总结,将一到十章的内容之串起来,实操一遍,接下来我们直接进入实操!一、创建一个工作空间首先,按下ctrl+shift+T打开终端,创建一个新的ros工作空间。#创建工作空间目录mkdir-psyj_ws/src#进入syj_......
  • 模型部署 - TensorRT - NVIDIA 讲 TensorRT - 8.6.1版本 -Plugin
                          ......
  • 模型部署 - TensorRT - NVIDIA 讲 TensorRT - 8.6.1版本 - 5种工具
                                          ......
  • Day68 代码随想录打卡|回溯算法篇---子集
    题目(leecodeT78):给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。方法:本题为求子集问题,采用回溯算法解决,与之前的组合与分割问题我们最后收集的是树上的叶子节点不同。子集......
  • 八股文笔记(持续更新)
    提示:本笔记基于【新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)】https://www.bilibili.com/video/BV1yT411H7YK?p=7&vd_source=a91dafe0f846ad7bd19625e392cf76d8总结面试职业技能总结如何找的合适的项目项目模块的深度学习如何深......
  • 算法学习day12(动态规划)
    一、不同的二叉搜索树二叉搜索树的性质:父节点比左边的孩子节点都大;比右边的孩子节点都小;由图片可知,dp[3]是可以由dp[2]和dp[1]得出来的。(二叉搜索树的种类和根节点的val有关)当val为1时,左边是一定没有节点的,因为左边的值都要比根节点小;只有右边会有n-val个节点。所以当va......
  • Reinforced Causal Explainer for GNN论文笔记
    论文:TPAMI2023 图神经网络的强化因果解释器论文代码地址:代码目录AbstractIntroductionPRELIMINARIESCausalAttributionofaHolisticSubgraph​individualcausaleffect(ICE)​*CausalScreeningofanEdgeSequenceReinforcedCausalExplainer(RC-Explaine......
  • java_day1学习笔记
    title:Java_Day_1学习笔记data:2024-07-09tags:Java学前简述学习方法为什么学?技术控?项目需求?作业要求?方法?现有技术能解决?现有技术能完美解决?新技术和知识点?根据需求有针对点的学习怎么学?基本原理和基本语法(不考虑细节,快速过一遍),不要先考虑细节,因为细......