Myer差分算法(Myer's diff algorithm)

Myer差分算法是一個時間複雜度爲O(ND)的diff算法,就以diff兩個字符串爲例,其中N爲兩個字符串長度之和,D爲兩個字符串的差異部分的總長度。這個算法首先發表在An O(ND) Difference Algorithm and Its Variations

Myer差分算法直接解決的問題是最長公共子序列(LCS)的等價問題——最小編輯腳本(SES)問題。當然了,這是論文中的表述,在我看來就是解決了最小編輯距離問題。Myer使用了圖來表述這個編輯過程,就以“ABC”和“CBA”這兩個字符串的編輯過程爲例:

在這裏插入圖片描述

源字符串排列在x軸上側,目標字符串排列在y軸左側。圖中的紅線是編輯過程,藍線和黃線暫時忽略。紅線共5條線段,以左上角爲原點,向右爲x軸正方向,向下爲y軸正方向,一個格子的長度爲1,那麼這5條線段的6個端點依次爲(0, 0)->(1,0)->(2,0)->(3,1)->(3,2)->(3,3)。水平向右的線段代表刪除線段終點x座標上的字符,豎直向下的線段代表插入線段終點y座標上的字符,斜向右下的線段代表不做編輯,保留線段終點座標上的字符,只有終點xy座標處的字符相等時才能這樣操作。那麼前面的5個線段分別代表了:-A、-B、C、+B、+A。我們把它列成一列:

-A
-B
 C
+B
+A

有沒有感到很熟悉呢?這和git的diff輸出格式是相似的:

$ git diff 1.txt 2.txt
diff --git a/1.txt b/2.txt
index b1e6722..da662e1 100644
--- a/1.txt
+++ b/2.txt
@@ -1,3 +1,3 @@
-A
-B
 C
+B
+A

要找一組連起來能從左上角抵達到右下角的線段還是很容易的:最基本的有兩組,先直抵達右上角,再直抵達右下角和先直抵達左下角,再直抵達右下角。反映在diff得結果上,前一組是把源字符串全部刪除再把目標字符串整個插入,後一組是把目標字符串整個插入再把源字符串全部刪除。但這樣的diff結果是沒有意義的,理想的diff能夠最大程度得保留兩個字符串相同的部分(LCS),最小化刪除和插入操作。表現在圖上的話,就是找一組從左上角抵達到右下角的線段,使得水平線段和豎直線段儘可能少,斜線線段儘可能多。怎麼做?

Myer使用了貪心算法來實現,下面我來描述一下算法的過程。描述算法的樣例字符串就用Myer論文中使用的樣例字符串,“ABCABBA”和“CBABAC”。

我們一步一步的來把源字符串編輯成目標字符串,每一步可以是刪除操作——添加一條水平線段,也可以是插入操作——添加一條豎直線段,添加斜線不算作編輯操作,只要有可能就可以按照規則儘可能地添加。這樣的話,每一步都會添加一個水平線段或者豎直線段,那麼問題就轉化爲了如何用最小的步數在圖上從左上角到達右下角。

若當前終點爲P(x,y),記k=x-y,我們可以發現,下一步如果插入水平線段,終點會變成P’(x+1,y),k’=x+1-y=k+1;下一步如果插入豎直線段,終點會變成P’’(x,y+1),k’’=x-(y+1)=k-1;而插入斜線不會影響終點的k值。總結下來就是:如果當前終點的k值爲k,那麼下一步終點的k值爲k+1或k-1。逆向使用這條規律:如果當前終點的k值爲k,上一步終點的k值爲k-1或k+1。

若當前的步數爲d,根據前面的推導,我們可以發現d和k之間是有聯繫的。d=0時,可能的終點k值只可能取0;d=1時,可能的終點k值可能取-1、1;d=2時,k可能取-2、0、2,依次類推,我們可以將對當前d值得可能k值使用下面這段程序輸出:

for (int k = -d; k <= d; k += 2)
{
    std::cout << k << std::endl;
}

我們把圖中所有k值相等的點,k=0,1,2,3…用線段連接起來,這些線段會是一組左上到右下方向的平行線。如下圖中的綠線:

在這裏插入圖片描述

有一條非常明顯的結論:每組k值相等的點(即在同一條綠線上的點)中,若某個x值大的點到達終點需要的步數爲d1,某個x值較小的點到達終點需要的步數爲d2,那麼d1<=d2一定成立。

那麼我們的貪心選擇就是:在當前步數d的當前k值線上的點,選擇能到達的那個x值極大的點,這個點能夠更快的抵達終點。這裏爲什麼說選擇呢?因爲爲了在步數爲d時到達k值爲k的線的話,在步數爲d-1時可能在k值爲k+1或k-1的線上,有兩個可能,所以需要我們選擇從哪條線上走到k值爲k的線上。

用代碼和註釋展示的話,基本流程就是這樣的:
(若源字符串長度爲M,目標字符串長度爲N,很顯然,最優的步數一定不會比M+N更差,所以d的上限是M+N。)


for (int d = 0; d <= M + N; d++)
{
    for (int k = -d; k <= d; k += 2)
    {
        // find a max-x point in current k line
        // it depends on max-x point in k-1 line & k+1 line
    }
}

我們用一個std::vecotr<std::map<int,int>> v來保存每一步d的每一個k值所能抵達的x值極大的那個點(這裏用std::map只是爲了方便說明原理,是因爲k值可能爲負,實際代碼千萬別用map,否則數據量大了以後速度之慢和內存消耗都非常驚人),當走到下一步時我們會用得到上一步所有k值能到達的x值最大的點。

如何選擇呢?如果v[d-1][k-1]=x1,v[d-1][k+1]=x2,從k-1線到達k線的話,是添加了一條水平線段,終點x’=x1+1,而從k+1線到達k線的話,是添加了一條豎直線段,終點x’’=x2。如果x1+1<x2,我們選擇從k+1線向下走一步,如果x1+1>x2,我們選擇從k-1線向右走一步,如果x1+1=x2呢,我們選擇從k+1線向下走一步,這是爲了能讓diff結果看起來更直觀一些(先刪除後插入更直觀)。因此,如果x1<x2(由於x值只能取整,所以等價於x1+1<=x2),從k+1線向下走到達k線,否則從k-1線向右走到達k線。要記得到達k線後,要儘可能的添加斜線,以便得到更大的x值。

還有一點需要注意的是,在當前步數d的k值爲d時,d-1步並沒有到達過k+1線(到達過k值最大的線爲d-1=k-1線),無法從k+1線到達k線。在當前步數d的k值爲-d時,d-1步並沒有到達過k-1線(到達過k值最小的線爲-(d-1)=-d+1=k+1線,無法從k-1線到達k線。

以上就是Myer差分算法大致原理和一些需要注意的點,演示程序見myers_diff倉庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章