1. 程式人生 > >程式設計之美--3.3計算字串的相似度

程式設計之美--3.3計算字串的相似度

許多程式會大量使用字串。對於不同的字串,我們希望能夠有辦法判斷其相似程式。我們定義一套操作方法來把兩個不相同的字串變得相同,具體的操作方法為:
  1.修改一個字元(如把“a”替換為“b”);
  2.增加一個字元(如把“abdd”變為“aebdd”);
  3.刪除一個字元(如把“travelling”變為“traveling”);

    比如,對於“abcdefg”和“abcdef”兩個字串來說,我們認為可以通過增加/減少一個“g”的方式來達到目的。上面的兩種方案,都僅需要一 次 。把這個操作所需要的次數定義為兩個字串的距離,而相似度等於“距離+1”的倒數。也就是說,“abcdefg”和“abcdef”的距離為1,相似度 為1/2=0.5。

  給定任意兩個字串,你是否能寫出一個演算法來計算它們的相似度呢?


  原文的分析與解法  

  不難看出,兩個字串的距離肯定不超過它們的長度之和(我們可以通過刪除操作把兩個串都轉化為空串)。雖然這個結論對結果沒有幫助,但至少可以知道,任意兩個字串的距離都是有限的。
  我們還是就住集中考慮如何才能把這個問題轉化成規模較小的同樣的子問題。如果有兩個串A=xabcdae和B=xfdfa,它們的第一個字元是 相同的,只要計算A[2,...,7]=abcdae和B[2,...,5]=fdfa的距離就可以了。但是如果兩個串的第一個字元不相同,那麼可以進行 如下的操作(lenA和lenB分別是A串和B串的長度)。

  1.刪除A串的第一個字元,然後計算A[2,...,lenA]和B[1,...,lenB]的距離。
  2.刪除B串的第一個字元,然後計算A[1,...,lenA]和B[2,...,lenB]的距離。
  3.修改A串的第一個字元為B串的第一個字元,然後計算A[2,...,lenA]和B[2,...,lenB]的距離。
  4.修改B串的第一個字元為A串的第一個字元,然後計算A[2,...,lenA]和B[2,...,lenB]的距離。
  5.增加B串的第一個字元到A串的第一個字元之前,然後計算A[1,...,lenA]和B[2,...,lenB]的距離。
  6.增加A串的第一個字元到B串的第一個字元之前,然後計算A[2,...,lenA]和B[1,...,lenB]的距離。

  在這個題目中,我們並不在乎兩個字串變得相等之後的字串是怎樣的。所以,可以將上面的6個操作合併為:
  1.一步操作之後,再將A[2,...,lenA]和B[1,...,lenB]變成相字串。
  2.一步操作之後,再將A[2,...,lenA]和B[2,...,lenB]變成相字串。
  3.一步操作之後,再將A[1,...,lenA]和B[2,...,lenB]變成相字串。

  這樣,很快就可以完成一個遞迴程式。

 

解法1:採用遞迴的方法(原文程式碼)

複製程式碼

public static int CalculateStringDistance(String Astr, int Abegin, int Aend,
        String Bstr, int Bbegin, int Bend){
    if(Abegin>Aend){
        if(Bbegin>Bend)
            return 0;
        else
            return Bend-Bbegin+1;
    }
    if(Bbegin>Bend){
        if(Abegin>Aend)
            return 0 ;
        else return Aend -Abegin+1;
    }
    if(Astr.charAt(Abegin)==Bstr.charAt(Bbegin)) //如果首個字元相等,遞迴比較剩餘字串
        return CalculateStringDistance(Astr,Abegin+1,Aend,Bstr,Bbegin+1,Bend);
    else{ //不相等的三種情況
        int t1 = CalculateStringDistance(Astr,Abegin+1,Aend,Bstr,Bbegin,Bend)+1;
        int t2 = CalculateStringDistance(Astr,Abegin+1,Aend,Bstr,Bbegin+1,Bend)+1;
        int t3 = CalculateStringDistance(Astr,Abegin,Aend,Bstr,Bbegin+1,Bend)+1;
        int tmin = t1<t2?t1:t2;
        return tmin<t3?tmin:t3;
    }
}

複製程式碼

main()

String Astr = "abcdefg"; 
String Bstr = "abcefg"; //B比A少一個字元
int dis = CalculateStringDistance(Astr,0,Astr.length()-1,Bstr,0,Bstr.length()-1);
System.out.println("兩個字串的距離為:"+dis);

結果:
兩個字串的距離為:1

上面的遞迴程式,有什麼地方需要改進呢?問題在於:在遞迴的過程中,有些資料被重複計算了。

 

解法2:動態規劃

原來的想法是在解法1的基礎上增加一個二維陣列,每次遞迴前加一個判斷,當資料已經被計算了,則直接從陣列中得出結果,否則在得到結果的同時將結果賦給陣列。但是發現結果有錯誤。因為在遞迴過程中不能保證結果實時儲存到陣列中。(這裡有疑問)

 

另一種動態規劃的方法,非遞迴。

參考:http://blog.csdn.net/flyinghearts/article/details/5605996

  我們知道適合採用動態規劃方法的最優化問題中的兩個要素:最優子結構重疊子問題。另外,還有一種方法稱為備忘錄(memoization),可以充分利用重疊子問題的性質。

  下面簡述一下動態規劃的基本思想。和分治法一樣,動態規劃是通過組合子問題的解而解決整個問題的。我們知道,分治演算法是指將問題劃分成獨立的子問題,遞迴地求解各子問題,然後合併子問題的解而得到原問題的解。與此不同,動態規劃適用於子問題不是獨立的情況,也就是各子問題包含公共的子子問題。在這種情況下,若用分治法則會做許多不必要的工作,即重複地求解公共的子問題。動態規劃演算法對每個子問題只求解一次,將其結果儲存在一張表中,從而避免每次遇到 各個子問題時重新計算答案。

動態規劃通常應用於最優化問題。此類問題可能有很多種可行解,每個解有一個值,而我們希望找出一個具有最優(最大或最小)值的解。稱這樣的解為該問題的“一個”最優解(而不是“確定的”最優解),因為可能存在多個取最優值的解。

  動態規劃演算法的設計可以分為如下4個步驟:

  1)描述最優解的結構。

  2)遞迴定義最優解的值。

  3)按自底向上的方式計算最優解的值。

  4)由計算出的結果構造一個最優解。

  第1~3步構成問題的動態規劃解的基礎。第4步在只要求計算最優解的值時可以略去。如果的確做了第4步,則有時要在第3步的計算中記錄一些附加資訊,使構造一個最優解變得容易。

  該問題明顯完全符合動態規劃的兩個要素,即最優子結構和重疊子問題特性。該問題的最優指的是兩個字串的最短距離,子問題的重疊性可以從原書中的那個遞迴演算法中看出。

  下面再來詳細說說什麼是重疊子問題。適用於動態規劃求解的最優化問題必須具有的第二個要素是子問題的空間要“很小”,也就是用來解原問題的遞迴 演算法可以反覆地解同樣的子問題,而不是總在產生新的子問題。典型地,不同的子問題數是輸入規模的一個多項式。當一個遞迴演算法不斷地呼叫同一問題時,我們說 該最優問題包含重疊子問題。相反地,適合用分治法解決的問題只往往在遞迴的每一步都產生全新的問題。動態規劃演算法總是充分利用重疊子問題,即通過每個子問 題只解一次,把解儲存在一個需要時就可以檢視的表中,而每次查表的時間為常數。

 

對於本問題

1. 當strA[i]等於strB[j]時 則c(i, j)=c(i-1, j-1)
2. 若strA[i]!=strB[j],
  (1)若將它們修改為相等,則對兩個字串至少還要操作c(i-1, j-1)次
  (2)若刪除strA[i]或在strB[j]後新增strA[i],則對兩個字串至少還要操作c(i-1, j)次
  (3)若刪除strB[j]或在strA[i]後新增strB[j],則對兩個字串至少還要操作c(i, j-1)次
此時c(i, j)=min( c(i-1, j-1), c(i-1, j), c(i, j-1) ) + 1

初始化c[i][0] = i ; c[0][j]=j,利用上述遞推公式可計算出c陣列,c[lenA][lenB]就是兩個字元的距離
這裡的初始化很重要。(為什麼這樣初始化?走讀一遍程式碼應該就明白意思了,大概意思如下圖)

程式程式碼:

複製程式碼

//解法2:動態規劃。非遞迴
public static int calculateStringDistance(String strA, String strB) {
    int lenA = (int) strA.length();
    int lenB = (int) strB.length();
    //c[i][j]存放的為strA從0到i-1與strB從0到j-1兩個子串的距離。(因為c的邊界不算)
    int[][] c = new int[lenA + 1][lenB + 1];
    // Record the distance of all begin points of each String
    // 初始化邊界
    for (int i = 0; i <= lenA; i++)
        c[i][0] = i;
    for (int j = 0; j <= lenB; j++)
        c[0][j] = j;
    c[0][0] = 0;
    //遞推求出所有c元素
    for (int i = 1; i <= lenA; i++)
        for (int j = 1; j <= lenB; j++) {
            if (strB.charAt(j - 1) == strA.charAt(i - 1))
                c[i][j] = c[i - 1][j - 1];
            else
                c[i][j] = minValue(c[i][j-1], c[i-1][j],c[i-1][j-1]) + 1;
        }

    for (int i = 0; i <= lenA; i++) {
        for (int j = 0; j <= lenB; j++) {
            System.out.print(c[i][j] + " ");
        }
        System.out.println();
    }
    return c[lenA][lenB];
}

//計算3個數的最小值
public static int minValue(int A, int B, int C) {
    int tmin = A < B ? A : B;
    return tmin < C ? tmin : C;
}

複製程式碼

main()

int dis3 = calculateStringDistance(Astr,Bstr);
System.out.println("兩個字串的距離為:"+dis3);

執行結果:

0 1 2 3 4 5 6 
1 0 1 2 3 4 5 
2 1 0 1 2 3 4 
3 2 1 0 1 2 3 
4 3 2 1 1 2 3 
5 4 3 2 1 2 3 
6 5 4 3 2 1 2 
7 6 5 4 3 2 1 
兩個字串的距離為:1

 

其他資料:http://www.cnblogs.com/yujunyong/articles/2004724.html

轉自:https://www.cnblogs.com/gnivor/articles/4604733.html