Myers差分算法的理解、实现、可视化
作者:Oto_G
QQ: 421739728
简介
本文章对Myers差分算法(Myers Diff Algorithm)进行了细致讲解,适合对Myers差分算法完全不了解的小白进行学习。
- 本文所使用的Myers工具在Myers View (myer-view.vercel.app)
- 源码在GitHub - G-haoyu/MyerView: Myers Diff Algorithm HTML Visual Version
Myers差分算法或者称为 Myers Diff Algorithm,其中的Myers是指发表该算法的作者;差分是英文Diff的直译,也可以理解为差别、区别;算法即为Algorithm。
该算法解决了LCS的等价问题SES,其在git diff、DNA序列比较等场景中都被广泛使用。
基础
首先Myers差分算法所解决的问题,用不那么严谨的话来说,就是解决了比较两个字符串之间的差异的问题。但就算如此,还是不够直观,我们先来看个例子。
差异的描述
两个字符串ABC和CBAB,找到两个字符串之间差异的问题可以具体化为:用删减、不变、增加这三个词描述字符串ABC变化到字符串CBAB的过程这样一个具体的问题。如上图,我列举出了两种从字符串ABC变化到字符串CBAB的方法(当然不止两种)。
左边那种的变化过程就可以描述为:增加CB,保持AB不变,删减C
右边那种的变化过程可以描述为:删减AB,保持C不变,增加BAB
注:一定要牢记,不管是目前理解还是在Myers算法中,比较字符串差异的过程都抽象为了字符串A变化到字符串B的过程,即可以理解为字符串A为原字符串,字符串B为新字符串。
比较差异的过程就是描述原字符串是如何变化到新字符串的过程。
为了后文方便描述,在本文中,我们将这些操作附上不同颜色:绿色表示增加、红色表示删减、白色表示不变。
好的差异比较
当然,通过刚刚的例子,我们也能发现,比较两个字符串的差异的结果有非常多种,那么接下来我们就需要定义什么是好的差异比较结果,也就是我们应该遵循什么标准来比较差异。
我们这次使用Myers在论文中所使用的字符串来进行演示,我在图中列举了对于从字符串A变化到字符串B的三种方式(当然还有许多方法),其中“比较1”是最佳的差异比较结果,即删减AB,保持C不变,增加B,保持AB不变,删减B保持A不变,增加C。
可以对照着图中这三个结果在草稿纸上过一遍变化过程,我们这里先看”比较1“和”比较2“,它们俩得出的结果非常相似,都删减了三个字符,增加了两个字符,同时有4个字符不变,但为什么“比较1”更好呢,可以仔细观察在第二个字符上,“比较1”选择了和前一个字符同样的操作即删减,而“比较2”则选择了和前一个字符不同的操作即“增加”。在直观感受上呢,“比较1”也比“比较2”更加清晰。再看“比较1”和“比较3”,虽然感官上“比较3”比“比较1”更加直观,但是完全失去了比较意义,所以我们依据这些优劣,确定好的差异比较的规则
差异应该表现为更连续的增加或者删减
相同内容应该尽可能多,即差异尽可能少
同时约定:当删减和增加都可选时,优先选择删减
注:对于第三条的约定,可以根据具体的应用场景进行修改,默认为优先删减
算法介绍
在了解完这些后,我们终于可以进入Myers差分算法的学习了。
首先来看看Myers在论文中是怎么描述自己设计的算法的,这段话截自《An O(ND) Difference Algorithm and Its Variations》
In this paper an O(ND) time algorithm is presented. Our algorithm is simple and based on an intuitive edit graph formalism.
翻译为:在论文中提出了一个时间复杂度为O(ND)的算法,我们的算法很简单,它基于一个直观的编辑图形式主义。
也就是Myers差分算法是通过构造一个名为编辑图的东西来解决差异比较问题,同时这个解决方法的速度也很快。
名词解释
下面先了解下在Myers的论文中出现的一些词的含义。图中有介绍的名词就不再介绍了。
LCS
SES
N
D
k:
- 可以理解为截距,它是通过x - y算出来的,看右边折线图中x轴(上方横轴)被原字符串覆盖,y轴(左侧纵轴)被新字符串覆盖,k = x - y,比如(3, 1)点所在的k = 3 - 1,即k = 2
snake
- 指代两黄色端点间的线段,如(1, 0)到(2, 2)的线段就称为一个snake
D-path
- 从(0, 0)出发,到达D的线路,如2-path,就表示从(0, 0)出发到(2, 4) (2, 2) (3, 1)的三条路线
edit graph
- 编辑图,就是Myers算法的核心,其形式就是图右的折线图
对于k, snake, edit graph可能了解了是什么,但仍然无法对应到Myers差分算法本身,那么下面我们就来将这些名词联系起来,首先是论文中Myers推导出的两个定理。
两个定理
图中给出了定理的理解,论文中用数学归纳法对这两个定理进行了证明,有兴趣可以阅读原论文An O(ND) Difference Algorithm and Its Variations,这两个定理对下文理解编辑图绘制步骤非常重要,请理解后再向下阅读。
绘制编辑图
联系这些名词就要通过绘制编辑图的方式来进行。下面给出绘制编辑图的方法。
请仔细阅读绘制方法,可以结合下方给出的编辑图配合理解,绘制方法并没有给出绘制步骤,绘制步骤在两图片后。
了解完绘制方法,接下来就是最核心的绘制步骤,首先我们明确我们绘制的终止情况是到达右下角那个点,图中即为(3, 4)点,选取它作为终点的原因在了解完绘制步骤后就能体会到。
绘制步骤用流程图给出
整个绘制编辑图的过程就是以内外两层循环为主进行的,外层以差异数量的不断增加来循环,最终得到的差异数量即为两字符串的差异数量;内层循环则是在每一轮差异下对k ∈ [-d, d]这样一个区域进行搜索(结合定理1就实现步长为2的跳跃搜索)。目的就是走到终点,走的策略则被设计为严格符合好的差异比较的标准。可以通过我设计的Myers可视化工具Myers View (myer-view.vercel.app)来帮助理解。
理解了编辑图的绘制过程,我想,你已经对Myers算法了解了大半了,可以进行一些“大跨步”了,我们直接来看代码,这里借用简析Myers - 小雨心情给出的JavaScript版代码,非常感谢,我在其代码之上加入了详细的注释,同时对代码进行了细微的调整,可以在TODO中看到。
function myers(stra, strb) {
// 字符串 a 的长度为 n
let n = stra.length
// 字符串 b 的长度为 m
let m = strb.length
/*
动态规划回溯前轮计算结果用,结构为 k: x ,
存储的是该截距(k)目前能到达的最远端 x ,
且 k 满足公式 k = x - y
*/
let v = {
'1': 0
}
/*
存储的是每一步差异(d)中的所有截距(k)
能到达的最远端 x 值,用于计算差异路径(d-path)
结构为 d: {k : x}
*/
let vs = {
'0': { '1': 0 }
}
// 声明差异 d ,该值记录两字符串差异的大小
let d
loop:
// 差异d,最坏情况 n+m 即两字符串完全不同
for (d = 0; d <= n + m; d++) {
let tmp = {}
/*
斜线不计入循环,只有两个方向 → || ↓
这里使用剪枝思想,使k不用遍历全表
*/
for (let k = -d; k <= d; k += 2) {
/*
判断是否是通过 + 到达的待测点,+ 的情况为:
当前截距等于负差异(首次循环,也就是左边界)或者
当前截距不等于正差异(末次循环,也就是上边界)且
上一截距的x大于下一截距的x(体现优先删除)
*/
let down = ((k == -d) || ((k != d) && v[k + 1] > v[k - 1]))
/*
如果是 + 方式到的该截距,
则说明该截距的前一步是从上截距过来的,否则是下截距下来的
*/
let kPrev = down ? k + 1 : k - 1
// 获取前一步的坐标 xStart yStart
let xStart = v[kPrev]
let yStart = xStart - kPrev
// 获取可能的当前点坐标,如果是 + 方式则x轴坐标不变,否则横坐标加一
let xMid = down ? xStart : xStart + 1
// y轴通过 k = x - y 计算得出
let yMid = xMid - k
// 声明当前可能的坐标(还未考虑走斜线)
let xEnd = xMid
let yEnd = yMid
/*
考虑走斜线(对字符串a、b进行比较,
如果当前x、y所在字符串相同则走斜线)
*/
while(xEnd < n && yEnd < m && stra[xEnd] === strb[yEnd]) {
xEnd++
yEnd++
}
// 更新截距k所能到的最远端xEnd,yEnd不必记录可以计算得到
// 动态规划回溯子问题的实现
v[k] = xEnd
// 记录当前截距的最新端点
tmp[k] = xEnd
/*
如果 xEnd 和 yEnd 到达了各自字符串的末端,
说明路径寻找到了终点,可以结束寻找
*/
if (xEnd == n && yEnd == m) {
// 形成完整 d - k 端点表
vs[d] = tmp
// 生成 diff 路径
let snakes = solution(vs, n, m, d)
// 打印两字符串 diff
printRes(snakes, stra, strb)
// 完成 Myers diff
break loop
}
}
// 刷新当前差异下能到达的最远端
vs[d] = tmp
}
}
// 由后向前回溯
function solution(vs, n, m, d) {
// snakes存 + - 步骤
let snakes = []
// 存放当前搜索的位置
let p = { x: n, y: m }
// 两文本的差异数量已知,往前倒推步骤
for (; d > 0; d--) {
// 取出最后一步的差异所有能到达的点 v[k], k∈[-d, d]
let v = vs[d]
// 取出前一步的差异所有能到达的点
let vPrev = vs[d-1]
// 计算当前位置的截距,首次循环是终点所在截距k
let k = p.x - p.y
// 获取当前截距的坐标
let xEnd = v[k]
let yEnd = xEnd - k
/*
判断该步是通过 + 还是 - 操作得到的,分两类:
1、当前截距与负差异相同
1.1 这种情况说明当前差异除了走斜线以外,其余都是走 + 完成的(TODO: 可优化)
2、当前截距不等于正差异 且 前一步差异所到达的点中,
当前截距的上侧截距能到达的最远点的x值比下策截距能到达的最远点的x值大
2.1 该判断的后半部分保证了删除先于增加的设计要求
*/
let down = ((k == -d) || ((k != d) && (vPrev[k + 1] > vPrev[k - 1])))
// 如果是通过 + 到达的该点,则前一步的截距在上侧,即 k + 1 ,反之则 k - 1
let kPrev = down ? k + 1 : k - 1
// 获得真正的前驱点(已包含走斜线情况)
let xStart = vPrev[kPrev]
let yStart = xStart - kPrev
// 获得走斜线的开始点,形象的称为mid,(对于没有走斜线的情况,得到的就是当前点)
let xMid = down ? xStart : xStart + 1
let yMid = xMid - k
// 将当前前驱点、斜线开始点(LCS)、当前点的 x 值压栈入 snakes
snakes.unshift([xStart, xMid, xEnd])
// 更新当前计算的位置
p.x = xStart
p.y = yStart
}
return snakes
}
function printRes(snakes, stra, strb) {
let grayColor = '^'
let redColor = '-'
let greenColor = '+'
let consoleStr = ''
let args = []
let yOffset = 0
snakes.forEach((snake, index) => {
// 获取步骤的前驱(开始) x
let s = snake[0]
// 获取步骤的LCS开始x
let m = snake[1]
// 获取步骤的终点 x
let e = snake[2]
// LCS的起点(TODO: 可以不新增large变量,snake中记录的m已经记录了LCS的开始位置)
// let large = s
// 如果是第一个差异,并且差异的开始点不是字符串头(即两字符串在开始部分有相同子字符串)
// 只会在snakes的forEach中的一个出现
if (index === 0 && s !== 0) {
// 用灰色打印所有相同字符,直到s
for (let j = 0; j < s; j++) {
consoleStr += `%c${grayColor+stra[j]}`
args.push(grayColor)
// 记录b字符串的当前位置(yOffset类似游标)
yOffset++
}
}
// 如果该子串的差异是 - 操作
// 删除
if (m - s == 1) {
// 用红色打印删除的字符
consoleStr += `%c${stra[s]}`
args.push(redColor)
// TODO: 此处large可以省略
// large = m
// 如果该子串的差异是 + 操作
// 添加
} else {
consoleStr += `%c${strb[yOffset]}`
args.push(greenColor)
// b字符串当前位置继续右移
yOffset++
}
// LCS部分,当前终点位置 e 减去 LCS的开始位置,即为相同字串的长度
// 不变
// for (let i = 0; i < e - large; i++) {
for (let i = 0; i < e - m; i++) {
// TODO: 此处large可以使用m代替
consoleStr += `%c${stra[m+i]}`
args.push(grayColor)
// b字符串当前位置继续右移
yOffset++
}
})
conole.log(consoleStr, ...args)
}
// test部分
let s1 = 'ABCABBA'
let s2 = 'CBABAC'
myers(s1, s2)
读完代码后应该能够对Myers差分算法的实现方法有了一个系统的认识,当然其实Myers不仅仅给出了这一版本的算法思路,而且对它进行了优化,在这里就不细说了,大致给一个优化思路:编辑图的起点和终点是已知的,那么从终点向起点绘制编辑图是否也可行呢,那同时从起点和终点绘制编辑图是否也可行呢?
感谢
Myers差分算法的理解、实现、可视化的更多相关文章
- bsdiff差分算法
bsdiff的基本原理 bsdiff是由Conlin Percival开源的一个优秀的差分算法,而且是跨平台的.在Android系统中所使用的imgdiff本质上就是bsdiff. bsdiff的依据 ...
- 【matlab】运动目标检测之"背景差分算法“
clear; clc; i1=imread('D:\Work\1.png'); i2=imread('D:\Work\2.png'); i1=rgb2gray(i1); i2=rgb2gray(i2) ...
- 【蓝桥杯C/C++组】备赛基础篇之差分算法
一.个人理解 前面学习了前缀和算法,对于访问任意区间的速度是比较快的,但如果我们要修改某个区间的数呢,对于前缀和算法来说这还是有点棘手. 所以我们来学学新的算法:差分算法! 前缀和数组储存的是前n个数 ...
- openCV中直方图均衡化算法的理解
直方图均衡化就是调整灰度直方图的分布,即将原图中的灰度值映射为一个新的值.映射的结果直观表现是灰度图的分布变得均匀,从0到255都有分布,不像原图那样集中.图像上的表现就是对比度变大,亮的更亮,暗的更 ...
- 洛谷 [USACO17OPEN]Bovine Genomics G奶牛基因组(金) ———— 1道骗人的二分+trie树(其实是差分算法)
题目 :Bovine Genomics G奶牛基因组 传送门: 洛谷P3667 题目描述 Farmer John owns NN cows with spots and NN cows without ...
- SDUT OJ 数据结构实验之串一:KMP简单应用 && 浅谈对看毛片算法的理解
数据结构实验之串一:KMP简单应用 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Discuss Problem Descr ...
- POJ1523(割点所确定的连用分量数目,tarjan算法原理理解)
SPF Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 7406 Accepted: 3363 Description C ...
- 【转】浅谈对主成分分析(PCA)算法的理解
以前对PCA算法有过一段时间的研究,但没整理成文章,最近项目又打算用到PCA算法,故趁热打铁整理下PCA算法的知识.本文观点旨在抛砖引玉,不是权威,更不能尽信,只是本人的一点体会. 主成分分析(PCA ...
- Codeforces 191C (LCA+树上差分算法)
题面 传送门 题目大意: 给出一棵树,再给出k条树上的简单路径,求每条边被不同的路径覆盖了多少次 分析 解决这个问题的经典做法是树上差分算法 它的思想是把"区间"修改转化为左右端点 ...
随机推荐
- for 循环详解
学习目标: 掌握 for 循环的使用 学习内容: 1.for语法 for(初始化语句; boolean表达式; 循环后操作语句) { 循环体; } 流程图如下: 特点: 初始化语句:只在循环开始时执行 ...
- Exchange日志清理
1.清理日志--完整备份 Exchange Server 2013被部署在Windows Server 2012 及以上版本的操作系统中,使用操作系统内的"Windows Server Ba ...
- 基于kubernetes的分布式限流
做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ...
- Zookeeper启动问题记录——ZooKeeper audit is enabled.
问题原因是Zookeeper的日志服务默认关闭,在zoo.cfg文件中添加 audit.enable=true 保存后重启服务就能够正常启动. 另外,这个只影响Zookeeper的日志记录,不影响其他 ...
- 利用java反射机制实现List>转化为List
BEGIN; 最近在用hibernate做项目,由于后续的业务功能比较多,然后框架原设计没有使用到一对多.一对一等特性,(艹TA妹)没办法,只能用原生sql语句获得需要的结果集.但是返回过来的是一个L ...
- Revit二次开发之添加选项卡和按钮
我们日常在revit开发中经常会用到按钮,可以通过revitAPI提供的接口创建按钮,今天我简单介绍一下两种按钮,一种是单命令按钮,另一种是含下拉菜单的按钮,包括创建他们的方法. 实现方法 1.实 ...
- XCTF练习题---MISC---Banmabanma
XCTF练习题---MISC---Banmabanma flag:flag{TENSHINE} 解题步骤: 1.观察题目,打开附件 2.发现是一张斑马图片,这是典型的条形码啊,直接开网站识别 网站:h ...
- XCTF练习题---MISC---坚持60S
XCTF练习题---MISC---坚持60S flag:flag{DajiDali_JinwanChiji} 解题步骤: 1.观察题目,下载附件,是一个java文件 2.打开玩了一会,真鸡儿难,试着反 ...
- 漏洞复现:使用Kali制作木马程序
漏洞复现:使用Kali制作木马程序 攻击机:Kali2019 靶机:Win7 64位 攻击步骤: 1.打开Kali2019和Win7 64位虚拟机,确定IP地址在一个网段 2.确定好IP地址,进入Ka ...
- 使用Typora + 阿里云OSS + PicGo 打造自己的图床
使用Typora + 阿里云OSS + PicGo 打造自己的图床 为什么要打造图床? 让笔记远走高飞 试问以下场景: 我们要把 markdown 笔记放到某博客上,直接进行复制即可.但因你的图片存储 ...