1. 程式人生 > >最長遞增子序列問題的求解(LIS)

最長遞增子序列問題的求解(LIS)

 

  最長遞增子序列問題是一個很基本、較常見的小問題,但這個問題的求解方法卻並不那麼顯而易見,需要較深入的思考和較好的演算法素養才能得出良好的演算法。由於這個問題能運用學過的基本的演算法分析和設計的方法與思想,能夠鍛鍊設計較複雜演算法的思維,我對這個問題進行了較深入的分析思考,得出了幾種複雜度不同演算法,並給出了分析和證明。

一,   最長遞增子序列問題的描述

  設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<kmaK1<ak2<…<akm

。求最大的m值。

二,   第一種演算法:轉化為LCS問題求解

設序列X=<b1,b2,…,bn>是對序列L=<a1,a2,…,an>按遞增排好序的序列。那麼顯然X與L的最長公共子序列即為L的最長遞增子序列。這樣就把求最長遞增子序列的問題轉化為求最長公共子序列問題LCS了。


  最長公共子序列問題用動態規劃的演算法可解。設Li=< a1,a2,…,ai>,Xj=< b1,b2,…,bj>,它們分別為L和X的子序列。令C[i,j]為Li與Xj的最長公共子序列的長度。則有如下的遞推方程:

 這可以用時間複雜度為O(n2)的演算法求解,由於這個演算法上課時講過,所以具體程式碼在此略去。求最長遞增子序列的演算法時間複雜度由排序所用的O(nlogn)的時間加上求LCS的O(

n2)的時間,演算法的最壞時間複雜度為O(nlogn)+O(n2)=O(n2)。

三,   第二種演算法:動態規劃法
  設f(i)表示L中以ai為末元素的最長遞增子序列的長度。則有如下的遞推方程:

  這個遞推方程的意思是,在求以ai為末元素的最長遞增子序列時,找到所有序號在L前面且小於ai的元素aj,即j<i且aj<ai。如果這樣的元素存在,那麼對所有aj,都有一個以aj為末元素的最長遞增子序列的長度f(j),把其中最大的f(j)選出來,那麼f(i)就等於最大的f(j)加上1,即以ai為末元素的最長遞增子序列,等於以使f(j)最大的那個aj為末元素的遞增子序列最末再加上ai;如果這樣的元素不存在,那麼

ai自身構成一個長度為1的以ai為末元素的遞增子序列。

這個演算法由Java實現的程式碼如下:

public void lis(float[] L)

  {

         int n = L.length;

         int[] f = new int[n];//用於存放f(i)值;

         f[0]=1;//以第a1為末元素的最長遞增子序列長度為1;

         for(int i = 1;i<n;i++)//迴圈n-1次

         {

                f[i]=1;//f[i]的最小值為1;

                for(int j=0;j<i;j++)//迴圈i 次

                {

                       if(L[j]<L[i]&&f[j]>f[i]-1)

                              f[i]=f[j]+1;//更新f[i]的值。

                }

         }

         System.out.println(f[n-1]);             

              }



  這個演算法有兩層迴圈,外層迴圈次數為n-1次,內層迴圈次數為i次,演算法的時間複雜度

所以T(n)=O(n2)。這個演算法的最壞時間複雜度與第一種演算法的階是相同的。但這個演算法沒有排序的時間,所以時間複雜度要優於第一種演算法。

四,   對第二種演算法的改進

  在第二種演算法中,在計算每一個f(i)時,都要找出最大的f(j)(j<i)來,由於f(j)沒有順序,只能順序查詢滿足aj<ai最大的f(j),如果能將讓f(j)有序,就可以使用二分查詢,這樣演算法的時間複雜度就可能降到O(nlogn)。於是想到用一個數組B來儲存“子序列的”最大遞增子序列的最末元素,即有

B[f(j)] =aj

  在計算f(i)時,在陣列B中用二分查詢法找到滿足j<i且B[f(j)]=aj<ai的最大的j,並將B[f[j]+1]置為ai。下面先寫出程式碼,再證明演算法的證明性。用Java實現的程式碼如下:

lis1(float[] L)

{

    int n = L.length;

    float[] B = new float[n+1];//陣列B;

    B[0]=-10000;//把B[0]設為最小,假設任何輸入都大於-10000;

    B[1]=L[0];//初始時,最大遞增子序列長度為1的最末元素為a1

    int Len = 1;//Len為當前最大遞增子序列長度,初始化為1;

    int p,r,m;//p,r,m分別為二分查詢的上界,下界和中點;

    for(int i = 1;i<n;i++)

    {

        p=0;r=Len;

        while(p<=r)//二分查詢最末元素小於ai+1的長度最大的最大遞增子序列;

        {

           m = (p+r)/2;

           if(B[m]<L[i]) p = m+1; //當L[i]大於B[m]時,將p變為m+1,如果此時退出迴圈,以前已經比較過B[m+1]是大於L[i]的,所以用L[i]替換B[m+1]是符合前面敘述的B[m]儲存的是長度相同情況下結束值是最小值的。

           else r = m-1;

        }

        B[p] = L[i];//將長度為p的最大遞增子序列的當前最末元素置為ai+1;

        if(p>Len) Len++;//更新當前最大遞增子序列長度;

        

        

    }

    System.out.println(Len); 

}

  現在來證明這個演算法為什麼是正確的。要使演算法正確只須證如下命題:

命題1:每一次迴圈結束陣列B中元素總是按遞增順序排列的。

證明:用數學歸納法,對迴圈次數i進行歸納。

  當i=0時,即程式還沒進入迴圈時,命題顯然成立。

i<k時命題成立,當i=k時,假設存在j1<j2,B[j1]>B[j2],因為第i次迴圈之前陣列B是遞增的,因此第i次迴圈時B[j1]B[j2]必有一個更新,假設B[j1]被更新為元素ai+1,由於ai+1=B[j1]> B[j2],按演算法ai+1應更新B[j2]才對,因此產生矛盾;假設B[j2]被更新,設更新前的元素為s,更新後的元素為ai+1,則由演算法可知第i次迴圈前有B[j2]s< ai+1< B[j1],這與歸納假設矛盾。命題得證。

命題2B[c]中儲存的元素是當前所有最長遞增子序列長度為c的序列中,最小的最末元素,即設當前迴圈次數為i,有B[c]={aj|f(k)=f(j)=ck,ji+1ajak}(f(i)為與第二種演算法中的f(i)含義相同)

證明:程式中每次用元素ai更新B[c](c=f(i)),設B[c]原來的值為s,則必有ai<s,不然ai就能接在s的後面形成長度為c+1的最長遞增子序列,而更新B[c+1]而不是B[c]了。所有B[c]中存放的總是當前長度為c的最長遞增子序列中,最小的最末元素。

命題3設第i次迴圈後得到的pp(i+1),那麼p(i)為以元素ai為最末元素的最長遞增子序列的長度。

證明:只須證p(i)等於第二種演算法中的f(i)。顯然一定有p(i)<f(i)。假設p(i)<f(i),那麼有兩種情況,第一種情況是由二分查詢法找到的p(i)不是陣列B中能讓ai接在後面成為新的最長遞增子序列的最大的元素,由命題1和二分查詢的方法可知,這是不可能的;第二種情況是能讓ai接在後面形成長於p(i)的最長遞增子序列的元素不在陣列B中,由命題2可知,這是不可能的,因為B[c]中存放的是最末元素最小的長度為c的最長遞增子序列的最末元素,若ai能接在長度為L(L> p(i))的最長遞增子序列後面,就應該能接在B[L]後面,那麼就應該有p(i)=L,L> p(i)矛盾。因此一定有p(i)f(i),命題得證。

演算法的迴圈次數為n,每次迴圈二分查詢用時logn,所以演算法的時間複雜度為O(nlogn)。這個演算法在第二種演算法的基礎上得到了較好的改進。

五,   總結

 本論文只給出了計算解的大小而沒有給出構造解的方法,因為我認為計算解的大小的演算法已能給出對問題的本質認識,只要計算解大小的演算法設計出,構造解就只是技術細節的問題了,而我關心的是怎樣對問題得到很好的認識而設計出良好的演算法。以上幾種演算法已用Java實現,都能得到正確的結果。在設計和改進演算法時用到了基本的演算法設計和分析、證明的基本方法,很好的鍛鍊了設計與分析演算法的思維能力,讓我從感性上認識到演算法分析與設計的重要性,並且感受了演算法分析、設計和改進的樂趣。

理解如下:
B陣列中存該序列長度的最小末尾元素,下標為最長遞增序列長度,遍歷如下
5 3 6 9 8 12 44 28
5
3
3 6
3 6 9
3 6 8
3 6 8 12
3 6 8 12 44
3 6 8 12 28
即最大長度為5,序列為3 6 8 12 28