彻底理解动态规划:编辑距离

news2025/1/13 3:15:53

本篇的题目非常经典,几乎是面试必备,即,编辑距离问题,edit distance;

给定两个字符串word1以及word2,返回将word1转为word2需要的最少步骤,在每一步中你可以针对字符串word1进行以下操作:

  • 新增一个字符

  • 删除一个字符

  • 替换一个字符

假如word1是"horse",word2是“ros”,那么你的程序需要返回3,也就是说将word1转为word2至少需要三个步骤:

  1. 将word1中的第一个字符h替换为字符r:horse -> rorse,此时word1变为rorse,word1与word2前两个字符相等

  2. 将word1中的第三个字符r删掉:rorse -> rose,此时word1变为rose,word1与word2的前三个字符相等

  3. 将word1中的最后一个字符删掉:rose -> ros,此时word1与word2相等。

想一想该怎样用动态规划解决这个问题。

选择与子问题

和之前的题目一样,你首先应该找出子问题是什么,子问题与原始问题的依赖关系是什么。

找出子问题的关键在于每一步的选择。

如果word1与word2的第一个字符相等,假设word1是hor、word2是hr,那么我们可以放心的排除掉两个字符串的第一个字符,即EditDistance("hor", "hr")一定等于EditDistance("or", "r"):

此时我们得到了一个子问题EditDistance("or", "r"),原始问题EditDistance("hor", "hr")的值等于该子问题。

真正有趣的是如果word1与word2的第一个字符不相等的情况,假设word1为“hor”,而word2为“ro”,此时根据该问题的规则针对word1的第一个字符有三种操作:

1,在word1的第一个字符前新增(Insert)一个字符r,此时word1变为rhor,由于此时word1 的第一个字符等于word2的第一个字符,可以放心的忽略掉,因此我们得到了子问题EditDistance("hor","o"),由于执行了一次新增操作,因此:

EditDistance("hor","ro") = EditDistance("hor","o") + 1

2,将word1的第一个字符删掉(Delete),此时word1变为“or”,我们得到了一个新的子问题EditDistance("or","ro"),由于执行了一次删除操作,因此:

EditDistance("hor","ro") = EditDistance("or","ro") + 1

3,将word1的第一个字符替换(Replace )为r,此时word1变为了“ror”,由于word1的第一个字符等于word2的第一个字符,因此可以放心的忽略掉,我们得到了一个新的子问题EditDistance("or","o"),由于执行了一次删除操作,因此:

EditDistance("hor","ro") = EditDistance("or","o") + 1

根据题目要求,我们需要得到最小的编辑距离,因此:

EditDistance("hor","ro") = min(EditDistance("hor","o"),
                               EditDistance("or","ro"),
                               EditDistance("or","o")) + 1

即:

可以看到,如果word1与word2的第一个字符如果不相等的话那么我们会得到三个子问题,取这三个子问题的最小值然后加1就是原始问题的解。

现在我们找到了子问题与原始问题之间的依赖关系。

实际上,根据上述讨论我们还可以进一步扩展从而得到完整的状态空间树。

从这棵树中可以看到最小的编辑距离是2。

现在你应该清楚的知道该怎样我们是怎样一步步将问题不断的分解为更小的子问题,然后利用子问题的解来得到原始问题的解了。

自顶向下递归代码

上图中每个方框都是一个子问题,决定一个子问题的因素在于word1与word2当前处理到了哪个位置,假设对word1处理到了第i个位置,对word2处理到了第j个位置,因此我们可以对问题进行定义:

int EditDistance(int i, int j);

该函数表示从i到word1的末尾形成的字符串与从j从word2的末尾形成的字符串的编辑距离。

因此如果调用该函数时我们应该这样使用:

EditDistance(0, 0);

有了该定义与上述分析,你可以轻而易举的写出这样的递归代码:

string word1;
string word2;

int EditDistance(int i, int j) {
  if (i == word1.length() && j == word2.length()) return 0;
  if (i == word1.length()) return word2.length() - j;
  if (j == word2.length()) return word1.length() - i;

  if (word1[i] == word2[j]) return EditDistance(i + 1, j + 1);
  else {
    return min(EditDistance(i + 1, j + 1), min(
               EditDistance(i, j + 1),
               EditDistance(i + 1, j))) + 1;
  }
}

我们将word1与word2声明为全局变量,这样你可以清楚的看到决定EditDistance函数值的因素只有这两个参数i和j,i的取值为[0, word1.length()],j的取值为[0, word2.length()],也就是说子问题的个数只有(word1.length() + 1) * (word2.length() + 1) 个,上述递归代码存在大量重复计算问题,因此可以通过增加cache进行优化,这个改动就留给大家啦。

接下来我们着手将自顶向下的递归代码改为自底向上的动态规划代码。

自底向上动态规划代码

由于子问题的个数只有(word1.length() + 1) * (word2.length() + 1) 个,因此可以定义一个相同大小的二维数组dp:

vector<vector<int>>dp(word1.length() + 1, vector<int>(word2.length() + 1, 0));

接下来我们要求解最小子问题,最小子问题就是上述递归代码的递归出口:

if (i == word1.length() && j == word2.length()) return 0;

该最小子问题的解包含在了dp数组的初始化中。

接下来的子问题是另外两个递归出口:

if (i == word1.length()) return word2.length() - j;
if (j == word2.length()) return word1.length() - i;

我们可以简单的构造出两种情况下的所有i和j来初始化数组dp,即:

for (int j = word2.length() - 1; j >= 0; j--)
  dp[word1.length()][j] = word2.length() - j;
for (int i = word1.length() - 1; i >= 0; i--)
  dp[i][word2.length()] = word1.length() - i;

最后我们利用两个for循环来构造出所有的i和j,从而将递归函数的最后一部分:

if (word1[i] == word2[j]) return EditDistance(i + 1, j + 1);
else {
  return min(EditDistance(i + 1, j + 1), min(
             EditDistance(i, j + 1),
             EditDistance(i + 1, j))) + 1;
}

放置在for循环中,并将对递归函数的调用替换为对数组dp的读写:

for (int i = word1.length() - 1; i >= 0; i--) {
  for (int j = word2.length() - 1; j >= 0; j--) {
    if (word1[i] == word2[j])
      dp[i][j] = dp[i + 1][j + 1];
    else
      dp[i][j] = min(dp[i + 1][j + 1], min(dp[i][j + 1], dp[i + 1][j])) + 1;
  }
}

最终,完整的动态规划代码为:

int minDistance(string word1, string word2) {
  vector<vector<int>>dp(word1.length() + 1, vector<int>(word2.length() + 1, 0));
  for (int j = word2.length() - 1; j >= 0; j--)
    dp[word1.length()][j] = word2.length() - j;
  for (int i = word1.length() - 1; i >= 0; i--)
    dp[i][word2.length()] = word1.length() - i;
  for (int i = word1.length() - 1; i >= 0; i--) {
    for (int j = word2.length() - 1; j >= 0; j--) {
      if (word1[i] == word2[j])
        dp[i][j] = dp[i + 1][j + 1];
      else
        dp[i][j] = min(dp[i + 1][j + 1], min(dp[i][j + 1], dp[i + 1][j])) + 1;
    }
  }

  return dp[0][0];
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143230.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

技术开发115

技术开发115 业务内容&#xff1a; 拖车用辅助脚、拖车用零件类、特殊车辆用车轴Sub。Assy产品、面向汽车产业的生产设备、面向建设机械的零部件类、面向汽车产业的检查夹具 公司简介&#xff1a; 董事长&#xff1a;佐藤安弘 资本金&#xff1a;4500万日元 员工数&#x…

shell第五天练习

题目&#xff1a; 1、编写函数&#xff0c;实现打印绿色OK和红色FAILED&#xff0c;判断是否有参数&#xff0c;存在为Ok&#xff0c;不存在为FAILED 2、编写函数&#xff0c;实现判断是否无位置参数&#xff0c;如无参数&#xff0c;提示错误 3、编写函数实现两个数字做为参数…

聊聊前端安全之CSRF

本文作者为奇舞团前端开发工程师一、什么是CSRFCSRF&#xff08;Cross Site Request Forgery&#xff0c;跨站域请求伪造&#xff09;&#xff0c;通常缩写为 CSRF。CSRF攻击是攻击者通过伪装成受信任用户向服务器发起各种请求&#xff0c;达到欺骗服务器接收并执行指令&#x…

【数据结构】顺序表

【本篇内容】线性表顺序表线性表线性表是n个具有相同特性的数据元素的有限序列。常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串......线性表在逻辑上是线性结构&#xff0c;但是在物理结构上并一定是连续的&#xff0c;线性表在物理上存储时&#xff0c;通常是以数组…

保证原子性的几种方式,你都知道吗???

1. 前言 今天主要是从实战 浅谈原理的角度来说下&#xff0c;并发线程下原子性的几种处理方式&#xff0c;好了废话不多说了&#xff0c;接下来让我们看看吧 2. 开始 在开始之前需要提问下大家, 代码i ; 能保持原子性吗&#xff1f;&#xff1f;&#xff1f; 是不是一句话就执…

C++——命名空间,输入输出,缺省参数

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;数据结构——二叉树 &#x1f525;<3>创作者&#xff1a;我的代码爱吃辣 ☂️<4>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<5>前言&#xff1a;补充C语言…

Java 诊断利器 Arthas JVM命令

一、jvm 相关命令介绍 命令说明dashboard当前系统的实时数据面板getstatic查看类的静态属性heapdumpdump java heap, 类似 jmap 命令的 heap dump 功能jvm查看当前 JVM 的信息logger查看和修改 loggermbean查看 Mbean 的信息memory查看 JVM 的内存信息ognl执行 ognl 表达式per…

【Android春招】Android基础day2

一、填空题 1&#xff0e;除了开启开发者选项之外&#xff0c;还需打开手机上的 _ 开关&#xff0c;然后才能在手机上调试App。 USB调试 2&#xff0e;App开发的两大技术路线包括 _和混合开发。 原生开发 3&#xff0e;App工程的编译配置文件名为 _。 build.gradle 4&#xff0…

挂载光盘,配置yum源并且安装http软件包 ansible(4)

目录 一、挂载本地光盘到/mnt 二、配置yum源 一、挂载本地光盘到/mnt 第一步&#xff1a; 使用mount模块 注&#xff1a;fstype代表文件格式 二、配置yum源 第一种方法&#xff1a; 使用yum_repository模块 检验是否有对应文件在受控主机node1受控主机&#xff1a;node2受控…

Zipkin数据持久化配置

上一篇我们了解了Zipkin的基础知识以及Zipkin的服务端搭建。 在使用过程中很多同学发现了他的秘密&#xff0c;Zipkin模式将数据保存在内存中&#xff0c;当我们重启后&#xff0c;追踪数据便会丢失。其实&#xff0c;Zipkin也支持将追踪数据保存到MySql或者ES中。 持久化到M…

大数据基础平台搭建-(四)HBbase集群HA+Zookeeper搭建

大数据基础平台搭建-&#xff08;四&#xff09;HBbase集群HAZookeeper搭建 大数据平台系列文章&#xff1a; 1、大数据基础平台搭建-&#xff08;一&#xff09;基础环境准备 2、大数据基础平台搭建-&#xff08;二&#xff09;Hadoop集群搭建 3、大数据基础平台搭建-&#xf…

Linux学习笔记——Nginx安装部署

5.3、Nginx安装部署 5.3.1、简介 Nginx&#xff08;engine x&#xff09;是一个高性能的HTTP和反向代理Web服务器&#xff0c;同时也提供了IMAP/POP3/SMTP服务。 同Tomcat一样&#xff0c;Nginx可以托管用户编写的WEB应用程序成为可访问的网页服务&#xff0c;同时也可以作为…

二十七、Docker (3)

&#x1f33b;&#x1f33b; 目录一、java开发者Docker常用镜像容器创建&#xff08;本地&#xff09;1.1 资源上传1.2 还原部署1.2.1 部署jdk1.2.2 部署tomcat1.2.3 部署nginx1.2.4 部署mysql二、java开发者Docker常用镜像容器创建(官网拉取)2.1 jdk部署2.1.1 拉取jdk镜像2.1.…

verilog学习笔记- 7)verilog程序框架

目录 注释&#xff1a; 关键字&#xff1a; Verilog常用的关键字&#xff1a; 程序框架&#xff1a; 注释&#xff1a; Verilog HDL 中有两种注释的方式&#xff0c;一种是以“/*”符号开始&#xff0c;“*/”结束&#xff0c;在两个符号之间的语句都 是注释语句&#xff…

7-10 集合相似度

给定两个整数集合&#xff0c;它们的相似度定义为&#xff1a;Nc​/Nt​100%。其中Nc​是两个集合都有的不相等整数的个数&#xff0c;Nt​是两个集合一共有的不相等整数的个数。你的任务就是计算任意一对给定集合的相似度。 输出格式&#xff1a; 对每一对需要计算的集合&…

DaVinci:键 - 键混合器

调色页面&#xff1a;键Color&#xff1a;Key当选中一个键混合器节点时&#xff0c;键 Key调板上对应显示键混合器 Key Mixer的相关选项。键混合器节点默认有两个输入链接。可在键混合器节点上右击选择“添加”或“移除”一个输入。所有的输入链接和惟一的输出链接均可在键调板…

Elasticsearch在各大互联网公司大量真实的应用案例

国内现在有大量的公司都在使用 Elasticsearch&#xff0c;包括携程、滴滴、今日头条、饿了么、360安全、小米、vivo等诸多知名公司。 除了搜索之外&#xff0c;结合Kibana、Logstash、Beats&#xff0c;Elastic Stack还被广泛运用在大数据近实时分析领域&#xff0c;包括日志分…

SQL如何在数据库中执行

数据库的服务端&#xff0c;可分为执行器(Execution Engine) 和 存储引擎(Storage Engine) 两部分&#xff1a; 执行器负责解析SQL执行查询存储引擎负责保存数据 1 SQL如何在执行器中执行 # 查询用户ID大于50的用户的所有订单 SELECT u.id AS user_id, u.name AS user_name,…

详解回调函数

目录 前言 一、 实现一个简单的计算器 1.1 - 代码一 1.2 - 代码二 二、qsort 函数的介绍 三、改进冒泡排序函数 前言 回调函数就是一个通过函数指针调用的函数。如果把函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数&#xff0c;当这个指针被用来调用其…

微信公众号调用扫一扫功能

手把手教你调用微信扫一扫&#xff0c;三分钟包会_前端人的博客-CSDN博客_调用微信扫一扫 第一次搞公众号&#xff0c;还以为跟上回调用企业微信扫一扫一样。。。调起扫一扫功能的过程自然是不同的&#xff0c;要注意的地方还挺多&#xff0c;记录一下 。 其实&#xff0c;在使…