轉載:最長遞增子序列問題動態規劃解法及其優化

最長遞增子序列問題的求解
  最長遞增子序列問題是一個很基本、較常見的小問題,但這個問題的求解方法卻並不那麼顯而易見,需要較深入的思考和較好的算法素養才能得出良好的算法。由於這個問題能運用學過的基本的算法分析和設計的方法與思想,能夠鍛鍊設計較複雜算法的思維,我對這個問題進行了較深入的分析思考,得出了幾種複雜度不同算法,並給出了分析和證明。
  一, 最長遞增子序列問題的描述
  設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<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;
   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],這與歸納假設矛盾。命題得證。
  命題2:B[c]中存儲的元素是當前所有最長遞增子序列長度爲c的序列中,最小的最末元素,即設當前循環次數爲i,有B[c]={aj| f(k)=f(j)=c∧k,j≤i+1→aj≤ak}(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次循環後得到的p爲p(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)。這個算法在第二種算法的基礎上得到了較好的改進。

附:

例題:http://acm.fzu.edu.cn/problem.php?pid=1130

我的程序:

#include <stdio.h>
#include <memory.h>
int n,t;
int a[40005];
int b[40005];
int f[40005];
int m;//最長遞增子序列的長度
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
scanf("%d",&n);
while(n--)
{
scanf("%d",&t);
for(int i=0;i<t;i++)
{
scanf("%d",&a[i]);
//b[i]=a[i];
}
memset(f,0,sizeof(f));
for(int k=0;k<=t;k++)
b[k]=t+3;
b[0]=-10;
f[0]=1;
b[1]=a[0];
m=f[0];
int p,r,q;
for(int j=1;j<t;j++)
{
if(b[m]<a[j])
{
f[j]=m+1;
b[f[j]]=a[j];
}
else
{
p=0;q=m;
r=(p+q)/2;
while(p<=q)
{
if(b[r]<a[j])
p=r+1;
else
q=r-1;
r=(p+q)/2;
}
f[j]=r+1;
if(b[f[j]]>a[j])
b[f[j]]=a[j];
}
if(m<f[j])
m=f[j];
}
printf("%d\n",m);
}
return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章