文本相似度計算/文本比較算法


參考: 

文本比較算法Ⅰ——LD算法

文本比較算法Ⅱ——Needleman/Wunsch算法

文本比較算法Ⅲ——計算文本的相似度

文本比較算法Ⅳ——Nakatsu算法

 

目錄:

問題

LD算法

Needleman/Wunsch算法

Nakatsu算法

 

問題

字符串s1 和 字符串s2 的比較算法 ==> 相似度 or 差異性。

主流的算法有兩大類:

  • 基於編輯距離 ( Edit Distance),例如:LD算法;
  • 基於最長公共子串 ( Longest Common Subsequence),例如:Needleman/Wunsch算法等。

 

LD算法

LD算法(Levenshtein Distance)又稱為編輯距離算法(Edit Distance):以字符串A通過插入字符、刪除字符、替換字符變成另一個字符串B,其中,這三類操作的總次數可表示兩個字符串的差異。

  例如:字符串A:kitten如何變成字符串B:sitting。

    第一步:kitten——》sitten。k替換成s

    第二步:sitten——》sittin。e替換成i

    第三步:sittin——》sitting。在末尾插入g

  故kitten和sitting的編輯距離為3。

 

定義說明:

  LD(A,B)表示字符串A和字符串B的編輯距離。若LD(A, B)=0表示字符串A和字符串B完全相同;

  Rev(A)表示反轉字符串A;

  Len(A)表示字符串A的長度;

  A+B表示連接字符串A和字符串B;

 

  A=a1a2……aN,表示A是由a1a2……aN這N個字符組成,Len(A)=N

  B=b1b2……bM,表示B是由b1b2……bM這M個字符組成,Len(B)=M

  定義LD(i, j) = LD(a1a2……ai, b1b2……bj),其中0≤i≤N,0≤j≤M, ===> LD(N,M)=LD(A,B)

 

  

  對於1≤i≤N,1≤j≤M,有如下公式(動態規划的思想):

  若ai=bj,則LD(i,j) = LD(i-1,j-1) [無需替換 刪除 插入]

  若ai≠bj,則LD(i,j) = Min(LD(i-1,j-1)[可以在此基礎上經過替換變成另一字符串]LD(i-1,j),LD(i,j-1)[可以在此基礎上經過插入or刪除變成另一字符串])+1

  

舉例說明:

A=GGATCGA,B=GAATTCAGTTA,計算LD(A,B)

  第一步:初始化LD矩陣  

  

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1                      
G 2                      
A 3                      
T 4                      
C 5                      
G 6                      
A 7                      

 

 

  第二步:利用上述的公式,計算第一行

 

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2                      
A 3                      
T 4                      
C 5                      
G 6                      
A 7                      

 

 

  第三步,利用上述的公式,計算其余各行 

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

 

  則LD(A,B) = LD(7,11) = 5 

 

代碼實現(C++):

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.length();
        int m = word2.length();

        // 有一個字符串為空串
        if (n * m == 0) return n + m;

        // DP 數組
        int D[n + 1][m + 1];

        // 邊界狀態初始化
        for (int i = 0; i < n + 1; i++) {
            D[i][0] = i;
        }
        for (int j = 0; j < m + 1; j++) {
            D[0][j] = j;
        }

        // 計算所有 DP 值
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                int left = D[i - 1][j] + 1;
                int down = D[i][j - 1] + 1;
                int left_down = D[i - 1][j - 1];
                if (word1[i - 1] != word2[j - 1]) left_down += 1;
                D[i][j] = min(left, min(down, left_down));

            }
        }
        return D[n][m];
    }
};

  

  我們往往不僅僅是計算出字符串A和字符串B的編輯距離,還要能得出它們的匹配結果

  以上面為例A=GGATCGA,B=GAATTCAGTTA,LD(A,B)=5

  他們的匹配為:

    A:GGA_TC_G__A

    B:GAATTCAGTTA

  如上面所示,藍色表示完全匹配,黑色表示編輯操作,_表示插入字符或者是刪除字符操作。如上面所示,黑色字符有5個,表示編輯距離為5。

  利用上面的LD矩陣,通過回溯,能找到匹配字串。

  第一步:定位在矩陣的右下角  

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

 

  第二步:回溯單元格,至矩陣的左上角

    若ai=bj,則回溯到左上角單元格

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

 

    若ai≠bj,回溯到 (左上角、上邊、左邊) 里值最小的單元格,若有相同最小值的單元格,優先級按照左上角、上邊、左邊的順序

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

 

 

LD算法矩陣
    G A A T T C A G T T A
  0 1 2 3 4 5 6 7 8 9 10 11
G 1 0 1 2 3 4 5 6 7 8 9 10
G 2 1 1 2 3 4 5 6 6 7 8 9
A 3 2 1 1 2 3 4 5 6 7 8 8
T 4 3 2 2 1 2 3 4 5 6 7 8
C 5 4 3 3 2 2 2 3 4 5 6 7
G 6 5 4 4 3 3 3 3 3 4 5 6
A 7 6 5 4 4 4 4 3 4 4 5 5

 

    依照上面的回溯法則,回溯到矩陣的左上角

  第三步:根據三個不同方向的回溯路徑,寫出匹配字串

    若回溯到左上角單元格,將ai添加到匹配字串A,將bj添加到匹配字串B 

    若回溯到上邊單元格,將ai添加到匹配字串A,將_添加到匹配字串B

    若回溯到左邊單元格,將_添加到匹配字串A,將bj添加到匹配字串B

    搜索晚整個匹配路徑,匹配字串也就完成了

 

  從上面可以看出,LD算法在不需要計算出匹配字串的話,時間復雜度為O(MN),空間復雜度經優化后為O(M)

  不過,如果要計算匹配字符串的話,時間復雜度為O(MN),空間復雜度由於需要利用LD矩陣計算匹配路徑,故空間復雜度仍然為O(MN)。這個在兩個字符串都比較短小的情況下,能獲得不錯的性能。不過,如果字符串比較長的情況下,就需要極大的空間存放矩陣。例如:兩個字符串都是20000字符,則LD矩陣的大小為20000*20000*2=800000000Byte=800MB。在比較長字符串的時候,還有其他性能更好的算法。

 

 

Needleman/Wunsch算法 

該算法是基於最長公共子串(不一定連續出現)的文本比較算法。

實例說明:

對於字符串A=kitten,字符串B=sitting

最長公共子串為ittn(注:最長公共子串不需要連續出現,但一定是出現的順序一致),最長公共子串長度為4。

 

定義:

  LCS(A,B)表示字符串A和字符串B的最長公共子串的長度,LSC(A,B)=0表示兩個字符串沒有公共部分。

  Rev(A)表示反轉字符串A

  Len(A)表示字符串A的長度

  A+B表示連接字符串A和字符串B

  

  A=a1a2……aN,表示A是由a1a2……aN這N個字符組成,Len(A)=N

  B=b1b2……bM,表示B是由b1b2……bM這M個字符組成,Len(B)=M

  定義LCS(i,j)=LCS(a1a2……ai,b1b2……bj),其中0≤i≤N,0≤j≤M

 

  對於1≤i≤N,1≤j≤M,有:

  若ai=bj,則LCS(i,j)=LCS(i-1,j-1)+1

  若ai≠bj,則LCS(i,j)=Max(LCS(i-1,j-1), LCS(i-1,j), LCS(i,j-1))    ==> 類似編輯距離

 

計算LCS(A,B)的算法有很多,下面介紹的Needleman/Wunsch算法是其中的一種。和LD算法類似,Needleman/Wunsch算法用的都是動態規划的思想。在Needleman/Wunsch算法中還設定了一個權值,用以區分三種操作(插入、刪除、更改)的優先級。在下面的算法中,認為三種操作的優先級都一樣。故權值默認為1。

 

舉例說明:

A=GGATCGA,B=GAATTCAGTTA,計算LCS(A,B)

  第一步:初始化LCS矩陣

 

Needleman/Wunsch算法矩陣
    G A A T T C A G T T A
  0 0 0 0 0 0 0 0 0 0 0 0
G 0                      
G 0                      
A 0                      
T 0                      
C 0                      
G 0                      
A 0                      

 

  第二步:計算矩陣的第一行

 

Needleman/Wunsch算法矩陣
    G A A T T C A G T T A
  0 0 0 0 0 0 0 0 0 0 0 0
G 0 1 1 1 1 1 1 1 1 1 1 1
G 0                      
A 0                      
T 0                      
C 0                      
G 0                      
A 0                      

 

   第三步:計算矩陣的其余各行

 

Needleman/Wunsch算法矩陣
    G A A T T C A G T T A
  0 0 0 0 0 0 0 0 0 0 0 0
G 0 1 1 1 1 1 1 1 1 1 1 1
G 0 1 1 1 1 1 1 1 2 2 2 2
A 0 1 2 2 2 2 2 2 2 2 2 2
T 0 1 2 2 3 3 3 3 3 3 3 3
C 0 1 2 2 3 3 4 4 4 4 4 4
G 0 1 2 2 3 3 3 4 5 5 5 5
A 0 1 2 3 3 3 3 4 5 5 5 6

 

  則,LCS(A,B)=LCS(7,11)=6

  可以看出,Needleman/Wunsch算法實際上和LD算法是非常接近的。故他們的時間復雜度和空間復雜度也一樣。時間復雜度為O(MN),空間復雜度為O(MN)。空間復雜度經過優化,可以優化到O(M),但是一旦優化就喪失了計算匹配字串的機會了。代碼和LD算法幾乎一樣。

 

代碼(c++):

int needleman(char s1[], char s2[]){
    int len1 = s1.size();
    int len2 = s2.size();
    int i,j;
    vector<vector<int>> count(len1+1,vector<int>(len2+1, 0) );

    for(i=1;i<len1+1;i++){
        for(j=1;j<len2+1;j++){
            if(s1[i] == s2[j]){
                count[i][j] = count[i-1][j-1]+1;
            } else {
                count[i][j] = max(max(count[i-1][j-1],count[i][j-1]),count[i-1][j]);
            }
        }
    }
    return count[len1][len2];
}

 

 

  LD算法和Needleman/Wunsch算法的回溯路徑是一樣的。這樣找到的匹配字串也是一樣的。

  不過,Needleman/Wunsch算法和LD算法一樣,若要找出匹配字串,空間的復雜度就一定是O(MN),在文本比較長的時候,是極為耗用存儲空間的。故若要計算出匹配字串,還得用其他的算法。

 

  

 計算文本相似度

  在給定的字符串A和字符串B,LD(A,B)表示編輯距離,LCS(A,B)表示最長公共子串的長度。

  如何來度量它們之間的相似度呢?

  不妨設S(A,B)來表示字符串A和字符串B的相似度。那么,比較合理的相似度應該滿足下列性質。

  性質一:0≤S(A,B)≤100%,0表示完全不相似,100%表示完全相等

  性質二:S(A,B)=S(B,A)

  目前,網上介紹的各種相似度的計算,都有各自的不盡合理的地方。

  計算公式一:S(A,B)=1/(LD(A,B)+1)

    能完美的滿足性質二。

    當LD(A,B)=0時,S(A,B)=100%,不過無論LD(A,B)取任何值,S(A,B)>0,不能滿足性質一。

 

  計算公式二:S(A,B)=1-LD(A,B)/Len(A)

    當Len(B)>Len(A)時,S(A,B)<0。不滿足性質一。

    有人會說,當S(A,B)<0時,強制指定S(A,B)=0就解決問題了。

    問題是,S(A,B)=1-LD(A,B)/Len(A),而S(B,A)=1-LD(B,A)/Len(B)。當Len(A)≠Len(B)時,S(A,B)≠S(B,A)。不滿足性質二

    還有一個例子可以說明問題

    A="BC",B="CD",C="EF"

    S(A,B)=1-LD(A,B)/Len(A)=1-2/2=0

    S(A,C)=1-LD(A,C)/Len(A)=1-2/2=0

    A和B的相似度與A和C的相似度是一樣的。不過很明顯的是B比C更接近A

 

  計算公式三:S(A,B)=LCS(A,B)/Len(A)

    這個公式能完美的滿足的性質一

    不過當Len(A)≠Len(B)時,S(A,B)≠S(B,A)。不滿足性質二

    用一個例子說明問題

    A="BC",B="BCD",C="BCEF"

    S(A,B)=LCS(A,B)/Len(A)=2/2=100%

    S(A,C)=LCS(A,C)/Len(A)=2/2=100%

    A和B的相似度與A和C的相似度是一樣的。不過很明顯的是B比C更接近A

 

  上面是網上能找到的三個計算公式,從上面的分析來看都有各自的局限性。

 

  我們看例子:

  A=GGATCGA,B=GAATTCAGTTA,LD(A,B)=5,LCS(A,B)=6

  他們的匹配為:

    A:GGA_TC_G__A

    B:GAATTCAGTTA

 

  給出一個新的公式

  S(A,B)=LCS(A,B)/(LD(A,B)+LCS(A,B))

  這個公式能解決上述三個公式的種種不足。

  而LD(A,B)+LCS(A,B)表示兩個字符串A、B的最佳匹配字串的長度。這個是唯一的。

  還有注意的是LD(A,B)+LCS(A,B)和Max(Len(A),Len(B))這兩個並不完全相等。

  

Nakatsu算法

LD算法和LCS算法都是基於動態規划的。它們的時間復雜度O(MN)、空間復雜度O(MN)(在基於計算匹配字符串情況下,是不可優化的。如果只是計算LD和LCS,空間占用可以優化到O(M))。

Nakatsu算法在計算匹配字符串的情況下,有着良好的時間復雜度O(N(M-P))和空間復雜度O(N2),而且在采取適當的優化手段時,可以將空間復雜度優化到O(N)。

 

定義說明:

1. 設M=Len(A),N=Len(B),不失一般性,假設M≤N。

2. A=a1a2……aM,表示A是由a1a2……aM這M個字符組成

B=b1b2……bN,表示B是由b1b2……bN這N個字符組成

LCS(i,j)=LCS(a1a2……ai,b1b2……bj),其中1≤i≤M,1≤j≤N

3. L(k,i)=Min{j} Where LCS(i,j)=k 表示,所有與字符串a1a2……ai有長度為k的LCS的字符串b1b2……bj中j的最小值(這個可以看后面的例子好好理解)

 

為了推導L的計算,有下面幾個定理。

  定理一:任意的i,1≤i≤M。有L(1,i)<L(2,i)<L(3,i)……

  定理二:任意的i,1≤i≤M-1。任意的k,1≤k≤M。有L(k,i+1)≤L(k,i)

  定理三:任意的i,1≤i≤M-1。任意的k,1≤k≤M-1。有L(k,i)<L(k+1,i+1)

  定理四:如果L(k,i+1)存在,則L(k,i+1)的計算公式為

      L(k,i+1)=Min{Min{j},L(k,i)} Where {ai+1=bj And j>L(k-1,i)}

  上面四個定理證明從略。可以從上面四個定理推導出L的計算。

 

  故,L的計算公式為

    L(1,1)=Min{j} Where {a1=bj

    L(1,i)=Min{Min{j} Where {ai=bj},L(1,i-1)}   此時,i>1

    L(k,i)=Min{Min{j} Where {ai=bj  And j>L(k-1,i-1)},L(k,i-1)}   此時,i>1,k>1

    注:以上公式中,若找不到滿足Where后面條件的j,則j=MaxValue

      當i<k時,則L(k,i)=MaxValue

      MaxValue是一個常量,表示“不存在”

 

  舉例說明:A=GGATCGA,B=GAATTCAGTTA,計算LCS(A,B)

  第一步:初始化L矩陣,表格中V=MaxValue。

 

Nakatsu算法L矩陣
  i=1 i=2 i=3 i=4 i=5 i=6 i=7
k=1              
k=2 V            
k=3 V V          
k=4 V V V        
k=5 V V V V      
k=6 V V V V V    
k=7 V V V V V V  

 

 

  第二步:依據上面的計算公式,計算表格的其余單元格

 

Nakatsu算法L矩陣
  i=1 i=2 i=3 i=4 i=5 i=6 i=7
k=1 1 1 1 1 1 1 1
k=2 V 8 2 2 2 2 2
k=3 V V 11 4 4 4 3
k=4 V V V V 6 6 6
k=5 V V V V V 8 7
k=6 V V V V V V 11
k=7 V V V V V V V

 

  第三步:在矩陣中找尋對角線

     1、先找如下的對角線,對角線中有四個單元格的值是V(MaxValue)。不是本算法的合適答案

Nakatsu算法L矩陣
  i=1 i=2 i=3 i=4 i=5 i=6 i=7
k=1 1 1 1 1 1 1 1
k=2 V 8 2 2 2 2 2
k=3 V V 11 4 4 4 3
k=4 V V V V 6 6 6
k=5 V V V V V 8 7
k=6 V V V V V V 11
k=7 V V V V V V V

 

     2、再找右邊的一條對角線。

 

Nakatsu算法L矩陣
  i=1 i=2 i=3 i=4 i=5 i=6 i=7
k=1 1 1 1 1 1 1 1
k=2 V 8 2 2 2 2 2
k=3 V V 11 4 4 4 3
k=4 V V V V 6 6 6
k=5 V V V V V 8 7
k=6 V V V V V V 11
k=7 V V V V V V V

 

      對角線上的所有單元格的值都不是V(MaxValue)。故本對角線就是算法的求解。

      LCS(A,B)就是對角線的長度。故LCS(A,B)=6。

      本算法的精妙之處就在於這六個單元格的值所對應的字符串B的字符就是最長公共子串。

      最長公共子串:b1b2b4b6b8b11=GATCGA

 

      再將最長公共子串在兩個字符串中搜索一遍,能得出字符串的匹配字串。

        A:GGA_TC_G__A

        B:GAATTCAGTTA

        注:原本以為能很容易得出匹配字符串。不過現在看來還需費一番周折,也是考慮不周。不過已經有大概的解決方案,留待后文介紹。

      

  

  Nakatsu算法關鍵就是找尋滿足條件對角線(對角線的值沒有MaxValue),故計算的過程可以沿着對角線進行,先計算第一條對角線,看是否滿足對角線條件,滿足則退出,不滿足則繼續計算下一條對角線,直到計算出滿足條件的對角線。

  假設LCS(A,B)=P,則一共需要計算M-P+1條對角線,每條對角線的比較次數為N,則Nakatsu算法的時間復雜度為O((M-P+1)N),空間復雜度為O(M2),但由於計算順序的優化,可以將空間復雜度降為O(M),這應該是令人滿意的了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM