什么是最長遞增子序列呢?
問題描述如下:
設L=<a1,a2,…,an>是n個不同的實數的序列,L的遞增子序列是這樣一個子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
對于這個問題有以下幾種解決思路:
1、把a1,a2,...,an排序,假設得到a'1,a'2,...,a'n,然后求a的a'的最長公共子串,這樣總的時間復雜度為o(nlg(n))+o(n^2)=o(n^2);
2、動態規劃的思路:
另設一輔助數組b,定義b[n]表示以a[n]結尾的最長遞增子序列的長度,則狀態轉移方程如下:b[k]=max(max(b[j]|a[j]<a[k],j<k)+1,1);
這個狀態轉移方程解釋如下:在a[k]前面找到滿足a[j]<a[k]的最大b[j],然后把a[k]接在它的后面,可得到a[k]的最長遞增子序列的長度,或者a[k]前面沒有比它小的a[j],那么這時a[k]自成一序列,長度為1.最后整個數列的最長遞增子序列即為max(b[k] | 0<=k<=n-1);
實現代碼如下:
#include <iostream>
using namespace std;
int main()
{
int i,j,n,a[100],b[100],max;
while(cin>>n)
{
for(i=0;i<n;i++)
cin>>a[i];
b[0]=1;//初始化,以a[0]結尾的最長遞增子序列長度為1
for(i=1;i<n;i++)
{
b[i]=1;//b[i]最小值為1
for(j=0;j<i;j++)
if(a[i]>a[j]&&b[j]+1>b[i])
b[i]=b[j]+1;
}
for(max=i=0;i<n;i++)//求出整個數列的最長遞增子序列的長度
if(b[i]>max)
max=b[i];
cout<<max<<endl;
}
return 0;
}
顯然,這種方法的時間復雜度仍為o(n^2);
3、對第二種思路的改進:
第二種思路在狀態轉移時的復雜度為o(n),即在找a[k]前面滿足a[j]<a[k]的最大b[j]時采用的是順序查找的方法,復雜度為o(n).
設想如果能把順序查找改為折半查找,則狀態轉移時的復雜度為o(lg(n)),這個問題的總的復雜度就可以降到nlg(n).
另定義一數組c,c中元素滿足c[b[k]]=a[k],解釋一下,即當遞增子序列的長度為b[k]時子序列的末尾元素為c[b[k]]=a[k].
先給出這種思路的代碼,然后再對其做出解釋。
#include <iostream>
using namespace std;
int find(int *a,int len,int n)//若返回值為x,則a[x]>=n>a[x-1]
{
int left=0,right=len,mid=(left+right)/2;
while(left<=right)
{
if(n>a[mid]) left=mid+1;
else if(n<a[mid]) right=mid-1;
else return mid;
mid=(left+right)/2;
}
return left;
}
void fill(int *a,int n)
{
for(int i=0;i<=n;i++)
a[i]=1000;
}
int main()
{
int max,i,j,n,a[100],b[100],c[100];
while(cin>>n)
{
fill(c,n+1);
for(i=0;i<n;i++)
cin>>a[i];
c[0]=-1;// …………………………………………1
c[1]=a[0];// ……………………………………2
b[0]=1;// …………………………………………3
for(i=1;i<n;i++)// ………………………………4
{
j=find(c,n+1,a[i]);// ……………………5
c[j]=a[i];// ………………………………6
b[i]=j;//……………………………………7
}
for(max=i=0;i<n;i++)//………………………………8
if(b[i]>max)
max=b[i];
cout<<max<<endl;
}
return 0;
}
對于這段程序,我們可以用算法導論上的loop invariants來幫助理解.
loop invariant: 1、每次循環結束后c都是單調遞增的。(這一性質決定了可以用二分查找)
2、每次循環后,c[i]總是保存長度為i的遞增子序列的最末的元素,若長度為i的遞增子序
列有多個,剛保存末尾元素最小的那個.(這一性質決定是第3條性質成立的前提)
3、每次循環完后,b[i]總是保存以a[i]結尾的最長遞增子序列。
initialization: 1、進入循環之前,c[0]=-1,c[1]=a[0],c的其他元素均為1000,c是單調遞增的;
2、進入循環之前,c[1]=a[0],保存了長度為1時的遞增序列的最末的元素,且此時長度為1
的遞增了序列只有一個,c[1]也是最小的;
3、進入循環之前,b[0]=1,此時以a[0]結尾的最長遞增子序列的長度為1.
maintenance: 1、若在第n次循環之前c是單調遞增的,則第n次循環時,c的值只在第6行發生變化,而由
c進入循環前單調遞增及find函數的性質可知(見find的注釋),
此時c[j+1]>c[j]>=a[i]>c[j-1],所以把c[j]的值更新為a[i]后,c[j+1]>c[j]>c[j-1]的性質仍然成
立,即c仍然是單調遞增的;
2、循環中,c的值只在第6行發生變化,由c[j]>=a[i]可知,c[j]更新為a[i]后,c[j]的值只會變
小不會變大,因為進入循環前c[j]的值是最小的,則循環中把c[j]更新為更小的a[i],當
然此時c[j]的值仍是最小的;
3、循環中,b[i]的值在第7行發生了變化,因為有loop invariant的性質2,find函數返回值
為j有:c[j-1]<a[i]<=c[j],這說明c[j-1]是小于a[i]的,且以c[j-1]結尾的遞增子序列有最大的
長度,即為j-1,把a[i]接在c[j-1]后可得到以a[i]結尾的最長遞增子序列,長度為(j-1)+1=j;
termination: 循環完后,i=n-1,b[0],b[1],...,b[n-1]的值均已求出,即以a[0],a[1],...,a[n-1]結尾的最長遞
增子序列的長度均已求出,再通過第8行的循環,即求出了整個數組的最長遞增子序列。
仔細分析上面的代碼可以發現,每次循環結束后,假設已經求出c[1],c[2],c[3],...,c[len]的值,則此時最長遞增子序列的長度為len,因此可以把上面的代碼更加簡化,即可以不需要數組b來輔助存儲,第8行的循環也可以省略。
#include <iostream>
using namespace std;
int find(int *a,int len,int n)//修改后的二分查找,若返回值為x,則a[x]>=n
{
int left=0,right=len,mid=(left+right)/2;
while(left<=right)
{
if(n>a[mid]) left=mid+1;
else if(n<a[mid]) right=mid-1;
else return mid;
mid=(left+right)/2;
}
return left;
}
int main()
{
int n,a[100],b[100],c[100],i,j,len;//新開一變量len,用來儲存每次循環結束后c中已經求出值的元素的最大下標
while(cin>>n)
{
for(i=0;i<n;i++)
cin>>a[i];
b[0]=1;
c[0]=-1;
c[1]=a[0];
len=1;//此時只有c[1]求出來,最長遞增子序列的長度為1.
for(i=1;i<n;i++)
{
j=find(c,len,a[i]);
c[j]=a[i];
if(j>len)//要更新len,另外補充一點:由二分查找可知j只可能比len大1
len=j;//更新len
}
cout<<len<<endl;
}
return 0;
}