算法學習心得——動態規劃法實現最長公共子序列(LCS)

算法學習心得——動態規劃法實現最長公共子序列(LCS)

一.問題說明

欲定義最長公共子序列(Longest Common Subsequence)問題,首先需要引入兩個輔助性的概念,即:子序列的概念和公共子序列的概念。

    子序列的概念形式化爲:設X = <x1, x2,┅, xm>,若有1≤i1< i2< ┅ <ik≤m,使得Z=< z1,z2,┅, zk> = < xi1, xi2,┅, xik>,則稱Z是X的子序列,記爲Z<X。舉例來說明,比如:X=<A,B,C,B,D,A,B>, Z=<B,C,B,A>,  則有Z<X。

    然後介紹公共子序列的概念:設X,Y是兩個序列,且有Z<X和Z<Y,則稱Z是X和Y 的公共子序列。

    理解了子序列的概念和公共子序列的概念,那麼就可以來解釋最長公共子序列的概念了。若:若Z<X,Z<Y,且不存在比Z更長的X和Y 的公共子序列,則稱Z是X和Y 的最長公共子序列,記爲ZÎLCS(X , Y)。通俗易懂地講就是一個序列S,若分別是兩個或者多個已知序列的子序列,並且序列S是所有符合條件的子序列中最長的,那麼就可以稱S爲已知的最長公共子序列。

    然而對於最長公共子序列的理解通常會有兩種觀點:第一種觀點認爲通過動態規劃法求得的最長公共子序列有連續性質的要求,即所求的最長公共子序列只能由原序列中連續出現的若干個元素組成;第二種觀點則認爲對最長公共子序列沒有連續性質的要求,即最長公共子序列是由原序列中的若干個元素所組成的,這些元素只需要滿足最基本的相對順序即可。本文中所介紹的最長公共子序列則是基於第二種觀點。

   針對最長公共子序列問題的研究是因爲有其重要性的應用領域,最長公共子序列主要用於生物信息學中DNA序列的比較。由於DNA序列非常龐大,比較兩個或者若干個DNA序列的相似度就需要用到最長公共子序列。而且最長公共子序列也可以用於其他方面:比如比較兩段文字或者兩篇文章之間的相似程度,從而來辨別他們之間是否存在抄襲關係。

二.動態規劃法介紹

由於最長公共子序列算法是基於動態規劃法實現的,因此有必要介紹下動態規劃法。說起動態規劃法,顧名思義就是將原問題一層一層地分解爲規模逐漸減小的同類型的子問題。而這些子問題往往具有高度重複性,基於子問題的高度重複性,動態規劃法一般採用遞推的方式,從規模最小的子問題開始依次計算規模逐漸增大的子問題。爲了避免重複計算,通常是每次計算完後就把所得的結果保存起來。因此當再次遇到子問題時可以直接使用已經保存的結果,無須重新計算。

然而並不是所有的問題都適合動態規劃法,只有當一個問題的子問題具有高度重複性,同時滿足最優子結構(即:問題的最優解中包含着其每一個子問題的最優解)性質時,才能使用動態規劃法求解。

三.最長公共子序列算法思想

清楚了最長公共子序列的概念,瞭解了動態規劃法的精髓,那麼現在可以來研究最長公共子序列算法的主要思想了。最長公共子序列問題屬於多階段決策問題中求最優解的一類問題。解決這類問題最直接的辦法就是使用窮舉法:列出所有長度不超過原序列的子序列,從長到短依次進行檢查,看哪個序列滿足條件。但是這種解法的時間複雜度往往是趨向去指數級別的,這種方法雖說簡單易懂,但只能作爲理論存在,實際應用中一般不會考慮這種方法。

         分析最長公共子序列問題,記 Xi=﹤x1,⋯,xi﹥即X序列的前i個字符 (1≤i≤m)(前綴);Yj=﹤y1,⋯,yj﹥即Y序列的前j個字符 (1≤j≤n)(前綴)假定Z=﹤z1,⋯,zk﹥∈LCS(X , Y)。

若xm=yn(最後一個字符相同),則不難用反證法證明:該字符必是X與Y的任一最長公共子序列Z(設長度爲k)的最後一個字符,即有zk = xm = yn 且顯然有Zk-1∈LCS(Xm-1, Yn-1)即Z的前綴Zk-1是Xm-1與Yn-1的最長公共子序列。

若xm≠yn,則亦不難用反證法證明:要麼Z∈LCS(Xm-1, Y),要麼Z∈LCS(X, Yn-1)。

由於zk≠xm與zk≠yn其中至少有一個必成立,若zk≠xm則有Z∈LCS(Xm-1 , Y),類似的,

若zk≠yn 則有Z∈LCS(X ,Yn-1)

若xm=yn,則問題化歸成求Xm-1與Yn-1的LCS(LCS(X , Y)的長度等於LCS(Xm-1, Yn-1)的長度加1)

若xm≠yn 則問題化歸成求Xm-1與Y的LCS及X與Yn-1的LCS

LCS(X, Y)的長度爲:max{LCS(Xm-1 , Y)的長度, LCS(X , Yn-1)的長度}求LCS(Xm-1, Y)的長度與LCS(X , Yn-1)的長度

兩者都需要求LCS(Xm-1,Yn-1)的長度,另外兩個序列的LCS中包含了兩個序列的前綴的LCS,故問題具有最優子結構性質 考慮用動態規劃法。

    引進一個二維數組C,用C[i,j]記錄Xi與Yj的LCS的長度如果我們是自底向上進行遞推計算,那麼在計算C[i,j]之前,C[i-1,j-1], C[i-1,j]與C[i,j-1]均已計算出來。此時

根據X[i]=Y[j]還是X[i]¹Y[j],就可以計算出C[i,j]:

若X[i]=Y[j],則執行C[i,j]←C[i-1,j-1]+1;

若X[i]¹Y[j],進行下述判斷:

若C[i-1,j]≥C[i,j-1]則C[i,j]取C[i-1,j];

否則C[i,j]取C[i,j-1]。即有

C[i,j]=

爲了構造出LCS,使用一個m´n的二維數組b,

b[i,j]記錄C[i,j]是通過哪一個子問題的值求得的,以決定搜索的方向:

若C[i-1,j]≥C[i,j-1],則b[i,j]中記入“↑”;

若C[i-1,j]< C[i,j-1],則b[i,j]中記入“←”;(以上分析引自鄧建明老師算法課程第三章最長公共子序列課件)

四.算法實現及實驗分析

  最長公共子序列算法的實現是基於java語言的,開發工具是eclipse。CPU是Inter i5處理器,內存:3,系統是Windows xp

  LCS算法java實現

package seu.wjm;

 

import java.util.Random;

import java.util.Scanner;

 

/**

 * 基於動態規劃法的最長公共子序列

 * @author wjm

 *

 */

public class LCS {

    public static void main(String[] args) {

    Scannerinput = new Scanner(System.in);

    System.out.println("請輸入待比較的兩個序列的長度(整數):");

    int len =input.nextInt();

   

        //設置待比較的兩個序列的長度

    int subStrLen1 = len;

    int subStrLen2 = len;

   

        // 通過get_random_str(int)方法獲取隨機生成的字符串str1和str2

        String str1 = get_random_str(subStrLen1);

        String str2 = get_random_str(subStrLen2);

 

       

        // 構造二維數組記錄子問題str1[i]和str2[i]的LCS的長度

        int[][] arr = new int[subStrLen1 + 1][subStrLen2 + 1];

 

        //獲取系統當前時間(毫秒數)

        long startTime = System.nanoTime();

       

        // 最長公共子序列核心思想

        /**

         *            0                                                                    若i=0或j=0             

         * arr[i,j]=  arr[i-1,j-1]+1             若i,j>0且str1[i]==str2[j]

         *            max{arr[i,j-1],arr[i-1,j]} 若i,j>0且str1[i]!=str2[j]

         */

        for (int i = subStrLen1 - 1; i >= 0; i--) {

            for (int j = subStrLen2 - 1; j >= 0; j--) {

                if (str1.charAt(i) == str2.charAt(j)){

                    arr[i][j] = arr[i + 1][j +1] + 1;

 

                }

                else{

                    arr[i][j] = Math.max(arr[i+ 1][j], arr[i][j + 1]);

 

                }

            }

        }

 

        System.out.println("Str1:"+str1);

        System.out.println("Str2:"+str2);

        System.out.print("LCS:");

 

        /**

         * 用於計算LCS序列

         */

        int i = 0, j = 0;

        while (i < subStrLen1 && j <subStrLen2) {

            if (str1.charAt(i) == str2.charAt(j)) {

                System.out.print(str1.charAt(i));

                i++;

                j++;

            } else if (arr[i + 1][j] >= arr[i][j + 1]){

                i++;

                }

            else{

                j++;

                }

        }

        System.out.println();

        long endTime = System.nanoTime();

        System.out.println(" 算法所需時間: " + (endTime - startTime) + " ns");

    }

 

  

   

    //通過傳人的長度隨機生成指定長度的字符串,限制字符串是由小寫字母構成的

    public static String get_random_str(int len) {

        StringBuilder builder = new StringBuilder("abcdefghijklmnopqrstuvwxyz");

        //StringBuilder對象sb用於記錄最後字符串的內容

        StringBuilder sb = new StringBuilder();

        //生成一個隨機類對象random

        Random random = new Random();

        int ranges = builder.length();

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

        //隨機類對象random的nextInt(int)函數返回的是[0-ranges)之間隨機的一個整數

           sb.append(builder.charAt(random.nextInt(ranges)));

        }

        return sb.toString();

    }

}

 

程序截圖:

    經過實驗分析可得出以下結論:在得到的隨機字符串等長的前提下,最大公共子序列算法所需的時間複雜度和理論分析相符合。

五.學習心得

    本學期的算法課程使得我瞭解了動態規劃法的核心思想,同時在課下通過網絡和一些書籍(例如:編程之美,算法導論等)也學習了一些使用動態規劃法解題的案例,熟悉了動態規劃法解題的思路和步驟,對動態規劃法有了較爲深入的思考和認識。因此我覺得對於動態規劃法的理解是我學習算法設計課程的最大收穫,介於此,在參考了鄧建明老師的算法課件的前提下,使用java語言實現了LCS算法。在編程過程中,不僅自己的動手實踐能力得到了鍛鍊,同時自己停留在書本知識層面的理解進一步得到了實踐的驗證,使得自己對於動態規劃法的認識更爲深刻!

發佈了28 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章