首页 > 编程语言 >算法分析与设计大课程报告

算法分析与设计大课程报告

时间:2023-10-16 11:44:06浏览次数:42  
标签:编辑 算法 课程 字符串 操作 Rec 设计 row

问题描述

问题背景:

输入法自动更正:当我们输入了一个不正确的词时,输入法就可能自动给我们更正。例如下面的例子:

 

图 1

提出问题:为什么输入法能够选到正确的那个词呢?

我们的猜想是,可能输入法会找“长得像”的词作为他推荐给用户的,也就是更正的词。那么如何让计算机知道什么叫长得像呢?具体来讲,如何衡量字符串之间的相似程度呢?

 

问题分析

相似操作的衡量可以用编辑距离来作为标准。

编辑距离是指又称Levenshtein距离(莱文斯坦距离也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

它可以用来做DNA分析,拼字检测,抄袭识别等等。总是比较相似的,或多或少我们可以考虑编辑距离。

在概念中,我们可以看出一些重点那就是,编辑操作只有三种。插入,删除,替换这三种操作,我们有两个字符串,将其中一个字符串经过上面的这三种操作之后,得到两个完全相同的字符串付出的代价是什么就是我们要讨论和计算的。

比如上面的例子中,如果左侧的词想要变成“kitchen”,只需要进行一次编辑操作。

 

图 2

而想要变成“setting”就要进行更多的编辑操作。

 

图 3

这里,我们将编辑操作的次数叫做编辑距离。

然而,我们可以将一个词插入再将这个词删除,如此反复,进而他的编辑距离将无限大。因此,寻找最大编辑距离是没有太大意义的,我们要探究的是最小编辑距离。

 

算法过程描述

最小编辑距离的定义:

输入:长度为n的字符串s,长度为m的字符串t

输出:求出一组编辑操作O = < e1 , e2 , e3 , …… ed >,令 min|O |

字符串s经过O的操作后满足s = t

给出问题表示:

我们假设D[i,j]为字符串s[1…i]变为t[1…j]的最小编辑距离

 

图 4

递推关系的建立:

分析最优结构:

考察末尾元素:删除

 

图 5

删除操作,把s串的最后一位删掉,此时的子问题是寻找D[i-1,j]的值

 

图 6

此时我们所求的D[i,j]应该等于其子问题的 D[i-1,j]加上本身进行的删除操作。

进而我们得到递推关系式D[i,j] = D[i-1,j] + 1

考察末尾元素:插入

插入操作:在s字符串目前最后一位的后面插入一个字符,这个值应该是t字符串的最后一个字符。

 

图 7

此时我们所求的D[i,j]应该等于其子问题的 D[i,j-1]加上本身进行的插入操作。

 

图 8

进而我们得到递推关系式D[i,j] = D[i,j-1] + 1

考察末尾元素:替换

替换操作:在s字符串目前最后一位替换成其他的一个字符,这个字符应该是t字符串的最后一个字符。

 

图 9

如果是s[i] = t[i],则不需要替换,否则需要替换。

当需要替换的时候,我们所求的D[i,j]应该等于其子问题的 D[i-1,j-1]加上本身进行的替换操作。

当不需要替换的时候,我们所求的D[i,j]应该等于其子问题的 D[i-1,j-1]

 

图 10

 

进而我们得到递推关系式

 

构造递推公式:

综上所述,可以将上述总结为如下递推公式:

 

图 11

自底向上计算:

初始化:

      D[i,0] = i 表示把长度为i的串变为空串至少需要i次操作。

   D[0,j] = j 表示把长度为0的串变为长度为j串至少需要j次操作。

于是我们可以得到这样一张表:

 

图 12

递推公式:

 

根据前面推导出的递推公式,我们就可以得知每个位置的内容应该是什么。

 

图 13

最优方案:

记录过程:

我们需要使用新的一个二维数组Rec用来记录每次选择的操作。

 

图 14

上表说明:

如果Rec[i,j] = U 则说明进行的是删除操作

如果Rec[i,j] = L 则说明进行的是插入操作

如果Rec[i,j] = LU 则说明进行的是替换操作或者无操作

最后我们得到这样一个Rec二维数组:

 

图 15

过程回溯:

现在我们需要根据Rec二维数组来给出我们把s串变为t串所需要的步骤。

从D[i][j]位置开始回溯:

如果当前位置为”L”:

 

图 16

则下一个位置应该变为Rec[i][j-1],意味着此s[i]应该插入t[j],而这一步的上一步,t的长度为j-1,最优操作为Rec[i][j-1]。

如果当前位置为”U”:

 

图 17

则下一个位置应该变为Rec[i-1][j],意味着此s[i]应该删除,而这一步的上一步,s的长度为i-1,最优操作为Rec[i-1][j]。

 

如果当前位置为”LU”:

 

图 18

则下一个位置应该变为Rec[i-1][j-1],意味着此s[i]应该被替换为t[j]或者不变,而这一步的上一步,s的长度为i-1,t的长度为j-1,最优操作为Rec[i-1][j-1]。 

算法复杂度分析

算法的伪代码:

MinDistance(s,t)

//求s和t的最小编辑距离

//输入:字符串s和t

//输出:s和t的最小编辑距离

新建D[0...n][0...m],Rec[0...n][0...m]两个二维数组

n ← length(s)

m ← length(t)

//初始化

for i ← 0 to n do

      D[i,0] ← i

      Rec[i,0] ← ‘U’

for j ← 0 to m do

      D[0,j] ← j

      Rec[i,0] ← ‘L’

//动态规划

for i ← 0 to n do

for j ← 0 to m do

          c ← 0

           if si ≠ tj then

                 c ← 1

           replace ← D[i - 1 ,j - 1]+ c

           delete ← D[i - 1 ,j ]+ 1

           insert ← D[i ,j - 1] + 1

           if replace = min{delete,insert,replace} then

                 D[i ,j ] ← D[i - 1 ,j - 1] + c

                 Rec[i ,j ] ← “LU”

           else if insert = min{delete,insert,replace} then

D[i ,j ] ← D[i ,j - 1] + 1

                 Rec[i ,j ] ← “L”

           else

D[i ,j ] ← D[i - 1,j] + 1

                 Rec[i ,j ] ← “U”


(Rec,s,t,n,m)

//寻找最优方案并输出

//输入:二维数组Rec,字符串s,t,位置下标n,m

//输出:操作序列

if n = 0 and m = 0 then

      return

if Rec[n,m] = “LU” then

      PrintMinDistance(Rec,s,t,n-1,m-1)

      if sn = tm  then

          print “无操作”

      else

           print “用tm替换sn”

else if Rec[n,m] = “U” then

      PrintMinDistance(Rec,s,t,n-1,m)

      print”删除sn”

else

      PrintMinDistance(Rec,s,t,n,m-1)

      print”插入tm” 

 

算法时间复杂度分析:

 

图 19

可以看出,整个算法的核心算法是动态规划,而动态规划使用了两个for循环来遍历二维数组的每个位置,所以其时间复杂度为O(nm)

 

算法改进

原来的算法是创建一个大小为s*t的矩阵。如果所有字符串加起来是1000个字符那么长的话,那么这个矩阵就会是1M。

对于计算编辑距离,如果我们不需要回溯,而是只想知道两者的最小编辑距离,那么上面的算法存储空间就是可以改进的,仔细观察你会发现递推公式的计算过程以及距离矩阵,你会发现当前距离的计算只和前一行以及当前行有关,即每次计算都只需要斜向的[i-1,j-1]、横向的[i,j-1]和纵向的[i-1,j]。而我们现在不需要知道中间结果,只需要最终结果,那么可以只要两行存储空间,进行迭代计算即可。现在只需要cur_row[]和pre_row[]两个一维数组即可。

现在的算法版本只使用2*t个元素。其结果是,不但内存占用更少,而且速度也变快了!因为这使得内存分配只需要很少的时间来完成。当两个字符串的长度都是1k左右时,新算法的效率是旧算法的两倍!

 

MinDistance_2(s,t)

//求s和t的最小编辑距离

//输入:字符串s和t

//输出:s和t的最小编辑距离

新建cur_row [0...n],pre_row [0...n]两个一维数组

m ← length(s)

n ← length(t)

//初始化

for i ← 0 to n do

      pre_row [i] ← i

 

//动态规划

for i ← 0 to m do

      cur_row [0] ← i

for j ← 0 to n do

          c ← 0

           if si ≠ tj then

                 c ← 1

           replace ← D[i - 1 ,j - 1]+ c

           delete ← D[i - 1 ,j ]+ 1

           insert ← D[i ,j - 1] + 1

           if replace = min{delete,insert,replace} then

                 cur_row [j] ← pre_row [j-1]+ c

           else if insert = min{delete,insert,replace} then

cur_row [j]← cur_row [j-1]+ 1

           else

cur_row [j] ← pre_row [j-1]+ 1

           swap(cur_row, pre_row)

return pre_row[n]

 

实验分析

实验结果:

 

图 20

 

为了方便记录,我将Rec数组设置为int型,

其中   1 表示  “插入” = L

           2 表示  “删除” = U

3 表示  “替换/无操作” = LU

通过人工进行验证,证明其操作步骤是正确的。

根据Rec数组记录的数据,我们就可以输出当前位置时进行的操作。由于查找步骤的算法是通过递归实现的,所以当输出时,就是按照正常的顺序来的。

图 21

实验分步分析:

初始化:

 

图 22

 

 

进行递归:

以D[1,1]举例:

  1. 如果是删除操作,则D[1,1] = D[0,1] + 1 = 2
  2. 如果是插入操作,则D[1,1] = D[1,0] + 1 = 2
  3. 如果是替换操作,先判断s[i] 和 t[i] 是否相同,不相同则D[1,1] = D[0,0] + 1 = 1

可见最小为1,则D[1,1] = 1

如果这三种操作后编辑距离相同,则优先选择替换操作。

最终会得到这样两个个表:

 

图 23

 

 

则最后D[i][j]就为最优解。

 

算法对比:

运行时间采集方式:

 

图 24

使用系统自带函数clock(),这个函数返回从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元数。在程序开始时先记录一个时钟数,在程序结束时再记录一个时钟数。将两个时钟数相减并除以一秒钟内CPU运行的时钟周期数(宏定义为CLOCKS_PER_SEC)就能得到程序运行时间。

测试数据规模:

8组随机字符串。

表 1

从图中我们可以看出,优化的算法显然更快。但由于算法执行过程中需要进行当前D二维数组的输出,所以越大的数组占用的时间输出的时间越长。排除输出的干扰,我们发现其实优化前后的执行时间并没有图中那样差距,单从算法的执行时间来说,两种算法基本相同。但是后一种占用空间更小。 

参考文献:

clayyjh. C语言 记录程序的执行时间[EB/OL] [2021-03-12].

https://www.cnblogs.com/clayyjh/p/14526665.html 

小乔流水人家. C语言 查看运行程序所需要的时间和占用的内存[EB/OL] [2017-02-22].

https://www.cnblogs.com/BeautyFuture/articles/6428551.html 

Penny呀 Levenshtein编辑距离算法的改进---剪枝优化 [EB/OL] [ 2021-01-17]. https://blog.csdn.net/PennyYu123/article/details/112747791 

程序员的自我反思 经典动态规划问题:最短编辑距离算法的原理及实现 [EB/OL] [ 2019-04-04].

https://blog.csdn.net/a553181867/article/details/89008264 

May Hacker 最小编辑代价(编辑距离问题改进版) [EB/OL] [ 2021-05-29]. https://blog.csdn.net/weixin_43889841/article/details/117381508 

附录:

#include<bits/stdc++.h>

using namespace std;

int D[100][100];

int Rec[100][100];

char str1[100] = "ABCBDAB", str2[100] = "BDCABA";

int min(int a, int b, int c) {

      int temp = a < b ? a : b;

      return temp < c ? temp : c;

}

 

void print1(int n,int m) {

      for(int i = 0 ; i < n ; i++) {

           cout << "[ ";

           for(int j = 0 ; j < m ; j++) {

                 cout << D[i][j] << ' ';

           }

           cout << "]" << endl;

      }

      cout << endl;

}

 

void print2(int n,int m) {

      for(int i = 0 ; i < n ; i++) {

           cout << "[ ";

           for(int j = 0 ; j < m ; j++) {

                 cout << Rec[i][j] << ' ';

           }

           cout << "]" << endl;

      }

      cout << endl;

}

void PrintMinDistance(int n,int m) {

      if(n == 0 &&  m ==  0) return;

      if(Rec[n][m] == 3) {

           PrintMinDistance(n-1,m-1);

           if(str1[n-1] == str2[m-1]) {

                 cout << "无操作" << endl;

           } else {

                 cout << str2[m-1] << "替换" << str1[n-1] << endl;

           }

      } else if(Rec[n][m] == 2) {

           PrintMinDistance(n-1,m);

           cout << "删除" << str1[n-1] << endl;

      } else {

           PrintMinDistance(n,m-1);

           cout << "插入" << str2[m-1] << endl;

      }

}

int MinDistance(char str1[], char str2[]) {

      int i, j, len1, len2;

      len1 = strlen(str1);

      len2 = strlen(str2);

      //初始化

      for(i = 0; i <= len1; i++) {

           D[i][0] = i;

           Rec[i][0] = 2;

      }

 

      for(j = 0; j <= len2; j++) {

           D[0][j] = j;

           Rec[0][j] = 1;

      }

      cout << "初始化后:" << endl;

      print1(i,j);

      //动态规划

      for(i = 1; i <= len1; i++)

           for(j = 1; j <= len2; j++) {

                 if(str1[i - 1] == str2[j - 1]) {   //因为D比str1和str2多了第0行和第0列,str是从下标0开始存数,而D[]是从下标1才开始真正存数,所以D[i]对应str[i - 1], 里一定要注意。

                      D[i][j] = D[i - 1][j - 1];

                      Rec[i][j] = 3;

                 } else {

                      int insert = D[i][j - 1] + 1;

                      int dele = D[i - 1][j] + 1;

                      int replace = D[i - 1][j - 1] + 1;

                      D[i][j] = min(insert, dele, replace);

                      if(D[i][j] == replace) Rec[i][j] = 3;

                      else if(D[i][j] == insert ) Rec[i][j] = 1;

                      else Rec[i][j] = 2;

                 }

           }

      cout << "完成动态规划后:" << endl;

      print1(i,j);

      cout << "Rec数组:" << endl;

      print2(i,j);

      cout << "操作步骤是:" << endl;

      PrintMinDistance(len1,len2);

      cout << endl;

      return D[len1][len2];

}

 

int main() {

//   cin >> str1 >> str2;

      printf("最小编辑距离是:%d\n", MinDistance(str1, str2));

}

 

标签:编辑,算法,课程,字符串,操作,Rec,设计,row
From: https://www.cnblogs.com/Sensei/p/17767011.html

相关文章

  • [算法分析与设计] 3. 并查集分析与反阿克曼函数
    Union-Find问题:给定\(n\)个元素,最初每个元素在一个集合中,有两种操作,union表示合并两个集合,find表示查询某个特定元素所在的集合。并查集是一种数据结构。其为每个集合寻找一个代表元,代表元可以是任意的,也可以随操作变化,但需要满足任何时刻一个集合的代表元是确定且唯一的。......
  • 数字电路硬件设计系列(十七)之上电时序控制电路
    1简介上电时序,也叫做Power-upSequence,是指电源时序关系。下面就是一系列电源的上电的先后关系:2方案介绍2.1电容实现延时采用不同的电容来控制上电延时时间的长短,具体的电路见下图:这种上电时序控制的方式,电路结构简单,但是延时时间难以精确的控制。在FPGA的电源......
  • java和c#里的TOTP统一算法
    基础说明本文根据RFC4226和RFC6238文档,详细的介绍HOTP和TOTP算法的原理和实现。两步验证已经被广泛应用于各种互联网应用当中,用来提供安全性。对于如何使用两步验证,大家并不陌生,无非是开启两步验证,然后出现一个二维码,使用支持两步验证的移动应用比如GoogleAuthenticat......
  • 树叶识别系统python+Django网页界面+TensorFlow+算法模型+数据集+图像识别分类
    一、介绍树叶识别系统。使用Python作为主要编程语言开发,通过收集常见的6中树叶('广玉兰','杜鹃','梧桐','樟叶','芭蕉','银杏')图片作为数据集,然后使用TensorFlow搭建ResNet50算法网络模型,通过对数据集进行处理后进行模型迭代训练,得到一个识别精度较高的H5模型文件。并基于Dja......
  • 学期:2023-2024-1 学号:20231426 《计算机基础与程序设计》第三周学习总结
    作业信息这个作业属于哪个课程2022-2023-1-计算机基础与程序设计这个作业要求在哪里2022-2023-1计算机基础与程序设计第一周作业这个作业的目标通过教材内容了解计算机信息层作业正文(https://www.cnblogs.com/hhaxx/p/17766468.html)教材学习内容总结......
  • 折半(二分)查找算法—高效搜索算法
    折半查找算法(BinarySearchAlgorithm)是一种高效的搜索算法,常用于已排序的数组或列表中进行查找操作。它的核心思想是通过比较中间元素与目标值的大小关系来确定目标值在数组的哪一部分,从而缩小搜索范围。本文将详细介绍折半查找算法的原理、实现以及应用场景。一、原理折半查找算......
  • 对设计模式的理解
    一切设计,都围绕着抽象与具体展开!大道至简!抽象:一般指接口。里面没有方法细节,只有方法签名。方法签名告诉你它能干什么,但不提供怎么干具体:所有具体类都应该是单一职责的。具体可以依赖抽象,程序运行过程中,会有该抽象的具体实现替代抽象。且具体类要符合最少知道原则,只开放必要的方......
  • 2023-2024-1 20231310《计算机基础与程序设计》第三周学习总结
    作业信息这个作业属于哪个课程https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP/homework/12999这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK03这个作业的目标计算机科学概论第2、3章,《C语言程序设计》第2章并完成云班课测试......
  • [算法学习笔记] ST表
    学习时间:2023/10/15CSP-S2023倒计时5days我竟然才会ST表简述ST表主要用于解决静态RMQ问题。实际上,凡是具备可重复贡献和结合律的问题,都可以用ST表来解决。ST表的优化方式和前缀和差分类似,采取预处理,每次可以做到\(O(1)\)时间复杂度的查询。因此,它适用于有大量查......
  • 开源项目 | 一款基于NodeJs+Vue3的强大的在线设计图片工具
     一、项目概述一款漂亮且功能强大的在线海报图片设计器,仿稿定设计。适用于海报图片生成、电商分享图、文章长图、视频/公众号封面等多种场景。二、技术特性丝滑的操作体验,丰富的交互细节,基础功能完善采用服务端生成图片,确保多端出图统一性,支持各种CSS特性简易AI......