算法之动态规划实战

news2024/10/7 16:20:37

目录

前言:

如何量化两个字符串的相似度?

 如何编程计算莱文斯坦距离?

如何编程计算最长公共子串长度? 

参考资料


前言:

     利用 Trie 树,可以实现搜索引擎的关键词提示功能,这样可以节省用户输入搜索关键词的时间。实际上,搜索引擎在用户体验方面的优化还有很多,比如你可能经常会用的拼写纠错功能。


如何量化两个字符串的相似度?

      如何量化两个字符串之间的相似程度呢?有一个非常著名的量化方法,那就是编辑距离(Edit Distance)。

      顾名思义,编辑距离指的就是,将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。对于两个完全相同的字符串来说,编辑距离就是 0

       比较著名的有莱文斯坦距离(Levenshtein distance)和最长公共子串长度(Longest common substring length)。其中,莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,最长公共子串长度只允许增加、删除字符这两个编辑操作。

       而且,莱文斯坦距离和最长公共子串长度,从两个截然相反的角度,分析字符串的相似程度。莱文斯坦距离的大小,表示两个字符串差异的大小;而最长公共子串的大小,表示两个字符串相似程度的大小。

     关于这两个计算方法,我举个例子给你说明一下。这里面,两个字符串 mitcmu mtacnu 的莱文斯坦距离是 3,最长公共子串长度是 4

 如何编程计算莱文斯坦距离?

       这个问题是求把一个字符串变成另一个字符串,需要的最少编辑次数。整个求解过程,涉及多个决策阶段,我们需要依次考察一个字符串中的每个字符,跟另一个字符串中的字符是否匹配,匹配的话如何处理,不匹配的话又如何处理。所以,这个问题符合多阶段决策最优解模型。

      贪心、回溯、动态规划可以解决的问题,都可以抽象成这样一个模型。要解决这个问题,我们可以先看一看,用最简单的回溯算法,该如何来解决。

     回溯是一个递归处理的过程。如果 a[i] b[j]匹配,我们递归考察 a[i+1] b[j+1]。如果 a[i] b[j]不匹配,那我们有多种处理方式可选:

  • 可以删除 a[i],然后递归考察 a[i+1] b[j]
  • 可以删除 b[j],然后递归考察 a[i] b[j+1]
  • 可以在 a[i]前面添加一个跟 b[j]相同的字符,然后递归考察 a[i] b[j+1];
  • 可以在 b[j]前面添加一个跟 a[i]相同的字符,然后递归考察 a[i+1] b[j]
  • 可以将 a[i]替换成 b[j],或者将 b[j]替换成 a[i],然后递归考察 a[i+1] b[j+1]

 具体的代码如下:


private char[] a = "mitcmu".toCharArray();
private char[] b = "mtacnu".toCharArray();
private int n = 6;
private int m = 6;
private int minDist = Integer.MAX_VALUE; // 存储结果
// 调用方式 lwstBT(0, 0, 0);
public lwstBT(int i, int j, int edist) {
  if (i == n || j == m) {
    if (i < n) edist += (n-i);
    if (j < m) edist += (m - j);
    if (edist < minDist) minDist = edist;
    return;
  }
  if (a[i] == b[j]) { // 两个字符匹配
    lwstBT(i+1, j+1, edist);
  } else { // 两个字符不匹配
    lwstBT(i + 1, j, edist + 1); // 删除a[i]或者b[j]前添加一个字符
    lwstBT(i, j + 1, edist + 1); // 删除b[j]或者a[i]前添加一个字符
    lwstBT(i + 1, j + 1, edist + 1); // 将a[i]和b[j]替换为相同字符
  }
}

     根据回溯算法的代码实现,我们可以画出递归树,看是否存在重复子问题。如果存在重复子问题,那我们就可以考虑能否用动态规划来解决;如果不存在重复子问题,那回溯就是最好的解决方法。

 

     在递归树中,每个节点代表一个状态,状态包含三个变量 (i, j, edist),其中,edist 表示处理到 a[i] b[j]时,已经执行的编辑操作的次数。 

     在递归树中,(i, j) 两个变量重复的节点很多,比如 (3, 2) (2, 3)。对于 (i, j) 相同的节点,我们只需要保留 edist 最小的,继续递归处理就可以了,剩下的节点都可以舍弃。所以,状态就从 (i, j, edist) 变成了 (i, j, min_edist),其中 min_edist 表示处理到 a[i] b[j],已经执行的最少编辑次数。

状态转移方程如下:


如果:a[i]!=b[j],那么:min_edist(i, j)就等于:
min(min_edist(i-1,j)+1, min_edist(i,j-1)+1, min_edist(i-1,j-1)+1)

如果:a[i]==b[j],那么:min_edist(i, j)就等于:
min(min_edist(i-1,j)+1, min_edist(i,j-1)+1,min_edist(i-1,j-1))

其中,min表示求三数中的最小值。     

状态转移表如下:

 具体的代码实现:


public int lwstDP(char[] a, int n, char[] b, int m) {
  int[][] minDist = new int[n][m];
  for (int j = 0; j < m; ++j) { // 初始化第0行:a[0..0]与b[0..j]的编辑距离
    if (a[0] == b[j]) minDist[0][j] = j;
    else if (j != 0) minDist[0][j] = minDist[0][j-1]+1;
    else minDist[0][j] = 1;
  }
  for (int i = 0; i < n; ++i) { // 初始化第0列:a[0..i]与b[0..0]的编辑距离
    if (a[i] == b[0]) minDist[i][0] = i;
    else if (i != 0) minDist[i][0] = minDist[i-1][0]+1;
    else minDist[i][0] = 1;
  }
  for (int i = 1; i < n; ++i) { // 按行填表
    for (int j = 1; j < m; ++j) {
      if (a[i] == b[j]) minDist[i][j] = min(
          minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]);
      else minDist[i][j] = min(
          minDist[i-1][j]+1, minDist[i][j-1]+1, minDist[i-1][j-1]+1);
    }
  }
  return minDist[n-1][m-1];
}

private int min(int x, int y, int z) {
  int minv = Integer.MAX_VALUE;
  if (x < minv) minv = x;
  if (y < minv) minv = y;
  if (z < minv) minv = z;
  return minv;
}

       当我们拿到一个问题的时候,我们可以先不思考,计算机会如何实现这个问题,而是单纯考虑人脑会如何去解决这个问题。人脑比较倾向于思考具象化的、摸得着看得见的东西,不适合思考过于抽象的问题。所以,我们需要把抽象问题具象化。那如何具象化呢?我们可以实例化几个测试数据,通过人脑去分析具体实例的解,然后总结规律,再尝试套用学过的算法,看是否能够解决。

如何编程计算最长公共子串长度? 

       最长公共子串作为编辑距离中的一种,只允许增加、删除字符两种编辑操作。从名字上,你可能觉得它看起来跟编辑距离没什么关系。实际上,从本质上来说,它表征的也是两个字符串之间的相似程度。

       每个状态还是包括三个变量 (i, j, max_lcs)max_lcs 表示 a[0...i] b[0...j]的最长公共子串长度。那 (i, j) 这个状态都是由哪些状态转移过来的呢?

     我们先来看回溯的处理思路。我们从 a[0] b[0]开始,依次考察两个字符串中的字符是否匹配。

  • 如果 a[i] b[j]互相匹配,我们将最大公共子串长度加一,并且继续考察 a[i+1] b[j+1]
  • 如果 a[i] b[j]不匹配,最长公共子串长度不变,这个时候,有两个不同的决策路线:
  • 删除 a[i],或者在 b[j]前面加上一个字符 a[i],然后继续考察 a[i+1] b[j]
  • 删除 b[j],或者在 a[i]前面加上一个字符 b[j],然后继续考察 a[i] b[j+1]

 反过来也就是说,如果我们要求 a[0...i] b[0...j]的最长公共长度 max_lcs(i, j),我们只有可能通过下面三个状态转移过来:

  • (i-1, j-1, max_lcs),其中 max_lcs 表示 a[0...i-1] b[0...j-1]的最长公共子串长度;
  • (i-1, j, max_lcs),其中 max_lcs 表示 a[0...i-1] b[0...j]的最长公共子串长度;
  • (i, j-1, max_lcs),其中 max_lcs 表示 a[0...i] b[0...j-1]的最长公共子串长度。

 状态转移方程代码如下:


如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
max(max_lcs(i-1,j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1));

如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
max(max_lcs(i-1,j-1), max_lcs(i-1, j), max_lcs(i, j-1));

其中max表示求三数中的最大值。

具体代码如下:


public int lcs(char[] a, int n, char[] b, int m) {
  int[][] maxlcs = new int[n][m];
  for (int j = 0; j < m; ++j) {//初始化第0行:a[0..0]与b[0..j]的maxlcs
    if (a[0] == b[j]) maxlcs[0][j] = 1;
    else if (j != 0) maxlcs[0][j] = maxlcs[0][j-1];
    else maxlcs[0][j] = 0;
  }
  for (int i = 0; i < n; ++i) {//初始化第0列:a[0..i]与b[0..0]的maxlcs
    if (a[i] == b[0]) maxlcs[i][0] = 1;
    else if (i != 0) maxlcs[i][0] = maxlcs[i-1][0];
    else maxlcs[i][0] = 0;
  }
  for (int i = 1; i < n; ++i) { // 填表
    for (int j = 1; j < m; ++j) {
      if (a[i] == b[j]) maxlcs[i][j] = max(
          maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]+1);
      else maxlcs[i][j] = max(
          maxlcs[i-1][j], maxlcs[i][j-1], maxlcs[i-1][j-1]);
    }
  }
  return maxlcs[n-1][m-1];
}

private int max(int x, int y, int z) {
  int maxv = Integer.MIN_VALUE;
  if (x > maxv) maxv = x;
  if (y > maxv) maxv = y;
  if (z > maxv) maxv = z;
  return maxv;
}

参考资料

本章内容来源于对王争大佬的《数据结构与算法之美》的专栏。

42 | 动态规划实战:如何实现搜索引擎中的拼写纠错功能?-极客时间

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

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

相关文章

【Python+pandas】学生测试成绩求和排序

✨博文作者 wangzirui32 &#x1f496; 喜欢的可以 点赞 收藏 关注哦~~ &#x1f449;本文首发于CSDN&#xff0c;未经许可禁止转载 &#x1f60e;Hello&#xff0c;大家好&#xff0c;我是wangzirui32&#xff0c;最近时间不够充足&#xff0c;鸽了几个月&#xff0c;这是2023…

【蓝桥杯第十三届C++B组】真题训练(5 / 8) - java写法

目录 4402.刷题统计 - 数学模拟 4403. 修剪灌木 - 思维 4404. X 进制减法 - 进制运算 贪心 4405. 统计子矩阵 - 前缀和 双指针​​​​​​​ 1、一维前缀和 2、二维前缀和 4406. 积木画 - dp 1、找规律dp 4402.刷题统计 - 数学模拟 4402. 刷题统计 - AcWing题库 思…

素数(prime)总结

素数的介绍 素数定义 质数(prime number)又称素数&#xff0c;有无限个。一个大于1的自然数&#xff0c;除了1和它本身外&#xff0c;不能被其他自然数整除&#xff0c;换句话说就是该数除了1和它本身以外不再有其他的因数;否则称为合数。 根据算术基本定理&#xff0c;每一个…

linux 文件夹多用户共享 解决文件删除的Permission denied

linux文件属性基础 Linux文件属主和属组 通过命令ls 可以查看文件属性&#xff1a; zcargo-MS-7D53:~/Desktop/parent_ceshi$ ll -lh total 16K drwxrwxr-x 2 zc zc 4.0K 1月 10 13:59 ceshi/ drwxrwxr-x 2 zc zc 4.0K 1月 10 13:59 ceshi02/以上展示命令的意思&#xff1…

大数据概念及Hadoop介绍

大数据概念及Hadoop介绍大数据概念数据存储单位大数据定义大数据的特点(5V特征)大数据部门组织架构分布式技术负载均衡故障转移伸缩性分布式技术总结分布式和集群的区别Hadoop介绍Hadoop核心组件Hadoop发展简史Hadoop特性优点与行业应用Hadoop特性优点行业应用Hadoop发行版本及…

14、Java网络编程

一、 网络编程概述Java是 Internet 上的语言&#xff0c;它从语言级上提供了对网络应用程序的支持&#xff0c;程序员能够很容易开发常见的网络应用程序。Java提供的网络类库&#xff0c;可以实现无痛的网络连接&#xff0c;联网的底层细节被隐藏在 Java 的本机安装系统里&…

【广度优先搜索遍历 + 染色法】判断二分图

还记得当时才大一&#xff0c;上离散数学课&#xff0c;当时觉得很不屑&#xff0c;就是觉得这学的什么玩弄啥用没有&#xff0c;但是其实很多的算法思想在解决题目时很有价值。 本文就将使用染色法解决判断二分图问题&#xff0c;使用广度优先搜索遍历和 Java 语言实现 一、题…

车辆逆行识别检测系统 opencv

车辆逆行识别检测系统通过opencvyolo网络深度学习技术&#xff0c;对现场画面中车辆逆向行驶行为进行检测抓拍预警。 OpenCV的全称是Open Source Computer Vision Library&#xff0c;是一个跨平台的计算机视觉处理开源软件库&#xff0c;支持与计算机视觉和机器学习相关的众多…

嵌入式 LINUX 驱动开发 day02 字符设备驱动 字符设备驱动 虚拟串口, 一个驱动支持多个设备

1. 驱动开发 字符设备驱动 代码&#xff1a; vser.c #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h>#include <linux/fs.h> /***** 设备相关信息 ******/ static unsigned int VSER_MAJOR 256; //主设备号…

青铜到王者,9本最好的Python从入门到进阶的书

春节长假还有2周了&#xff0c;是时候囤一些书充充电了&#xff01;新的一年群里很多小伙伴开始想学Python&#xff0c; 无论是准备转行的&#xff0c;还是想多学一份技能提高职场竞争力的&#xff0c;都想选择Python赛道&#xff0c;下面给大家推荐一些非常不错的Python入门到…

【Flask框架】—— 30 Flask-RESTful

前后端分离和前后端不分离 前后端不分离 在前后端不分离的应用模式中&#xff0c;前端页面看到的效果都是由后端控制&#xff0c;由后端渲染页面或重定向&#xff0c;也就是后端需要控制前端的展示&#xff0c;前端与后端的耦合度很高。 这种应用模式比较适合纯网页应用&…

iclr 2022 Compositional attention: Disentangling search and retrieval

Mittal S, Raparthy S C, Rish I, et al. Compositional attention: Disentangling search and retrieval[J]. arXiv preprint arXiv:2110.09419, 2021. 目录Mittal S, Raparthy S C, Rish I, et al. Compositional attention: Disentangling search and retrieval[J]. arXiv p…

【Kettle报错】kettle7.0链接MySQL显示:No appropriate protocol

【Kettle7.0链接本地MySQL数据库报错】 1. 问题描述 报错问题&#xff1a;kettle7.0报错&#xff0c;提示No appropriate protocol (protocol is disabled or cipher suites are inappropriate) 错误原因&#xff1a; Caused by: javax.net.ssl.SSLHandshakeException: No a…

Vue实现路由(Vue-router,参数传递,编程式路由导航)

目录 路由是什么&#xff1f; 怎么实现路由 第一步 创建一个文件夹 router &#xff0c;里面创建一个index.js 内容是 第二步 在main.js中引入和应用 router 第三步 在Vue中 通过两个标签进行配置 跳转路由时的参数传递 query params query参数和 param参数的区别 编…

原型链和JSON

对象的封装、继承和多态 封装、继承和多态是面向对象编程的三大特征&#xff0c;在JavaScript中也可以使用这些特征来实现面向对象的的编程。 封装是指将对象的属性和方法封装在对象内部&#xff0c;只提供必要的接口给外部访问。封装可以让我们隐藏对象的实现细节&#xff0c;…

项目管理:制定项目进度计划的好处有哪些?

项目管理计划确定了项目执行、监控及结束项目的整个过程&#xff0c;在项目开始之前&#xff0c;如能制定清晰的计划&#xff0c;并让项目成员都了解项目的目标和自己的责任&#xff0c;会对项目的推进有很大的帮助。 制定项目进度计划的好处有哪些&#xff1f; 1、目标导向…

高级树结构之二叉查找树

文章目录一 二叉查找树简介二 创建和插入操作三 查找操作3.1 查找思路3.2 代码实现四 删除操作4.1 情况讨论4.2 代码实现五 完整代码5.1 二叉查找树的结构5.2 完整代码内容一 二叉查找树简介 二叉查找树【二叉搜索树或是二叉排序树】 左子树中所有结点的值&#xff0c;均小于其…

(十一)devops持续集成开发——jenkins流水线发布一个docker harbor仓库版的前端vue项目

前言 本节内容&#xff0c;我们使用jenkins的流水线功能发布一个docker harbor私服仓库版的前端vue项目&#xff0c;延续前一节的内容&#xff0c;这里需要我们事先安装好一个docker harbor仓库用来存放我们项目的镜像&#xff0c;前端项目依然是通过nginx基础镜像构建&#x…

关于时间复杂度什么是时间复杂度

文章目录简介常见的Big-Oh简介 精确的考虑程序运行时间会使得寸步难行&#xff0c;而且毫无意义&#xff0c;所以可以用一种“概量”的概念来衡量运行时间&#xff0c;称之为“时间复杂度”。 时间复杂度的定义&#xff1a; 在一个完全立项状态下的计算机中&#xff0c;我们定…

【Sql Server】数据库的表变量和临时表的区别,并通过变量表随机生成姓名

作者&#xff1a;小5聊 简介&#xff1a;一只喜欢全栈方向的程序员&#xff0c;欢迎咨询&#xff0c;尽绵薄之力答疑解惑 公众号&#xff1a;有趣小馆&#xff0c;一个有趣的关键词回复互动功能 效果 1、表变量 1.1、表变量基本信息 1&#xff09;表变量本质是一个变量 是SQ…