轉載請注明原文地址:http://www.cnblogs.com/GodA/p/5180560.html
學習動態規划問題(DP問題)中,其中有一個知識點叫最長上升子序列(longest increasing subsequence),也可以叫最長非降序子序列,簡稱LIS。簡單說一下自己的心得。
我們都知道,動態規划的一個特點就是當前解可以由上一個階段的解推出, 由此,把我們要求的問題簡化成一個更小的子問題。子問題具有相同的求解方式,只不過是規模小了而已。最長上升子序列就符合這一特性。我們要求n個數的最長上升子序列,可以求前n-1個數的最長上升子序列,再跟第n個數進行判斷。求前n-1個數的最長上升子序列,可以通過求前n-2個數的最長上升子序列……直到求前1個數的最長上升子序列,此時LIS當然為1。
讓我們舉個例子:求 2 7 1 5 6 4 3 8 9 的最長上升子序列。我們定義d(i) (i∈[1,n])來表示前i個數以A[i]結尾的最長上升子序列長度。
前1個數 d(1)=1 子序列為2;
前2個數 7前面有2小於7 d(2)=d(1)+1=2 子序列為2 7
前3個數 在1前面沒有比1更小的,1自身組成長度為1的子序列 d(3)=1 子序列為1
前4個數 5前面有2小於5 d(4)=d(1)+1=2 子序列為2 5
前5個數 6前面有2 5小於6 d(5)=d(4)+1=3 子序列為2 5 6
前6個數 4前面有2小於4 d(6)=d(1)+1=2 子序列為2 4
前7個數 3前面有2小於3 d(3)=d(1)+1=2 子序列為2 3
前8個數 8前面有2 5 6小於8 d(8)=d(5)+1=4 子序列為2 5 6 8
前9個數 9前面有2 5 6 8小於9 d(9)=d(8)+1=5 子序列為2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我們可以看出這9個數的LIS為d(9)=5
總結一下,d(i)就是找以A[i]結尾的,在A[i]之前的最長上升子序列+1,當A[i]之前沒有比A[i]更小的數時,d(i)=1。所有的d(i)里面最大的那個就是最長上升子序列。話不多說,show me the code!下面是代碼實現的算法。
1 int LIS(int A[],int n) 2 { 3 int* d = new int[n]; 4 int len = 1; 5 int i,j; 6 for(i=0;i<n;i++) 7 { 8 d[i]=1; 9 for(j=0;j<i;j++) 10 { 11 if(A[j]<=A[i] && (d[j]+1)>=d[i]) 12 d[i]=d[j]+1; 13 } 14 if(d[i]>len) len=d[i]; 15 } 16 delete []d; 17 return len; 18 }
這個算法的時間復雜度為〇(n²),並不是最優的算法。在限制條件苛刻的情況下,這種方法行不通。那么怎么辦呢!有沒有時間復雜度更小的算法呢?說到這里了,當然是有的啦!還有一種時間復雜度為〇(nlogn)的算法,下面就來看看。
我們再舉一個例子:有以下序列A[]=3 1 2 6 4 5 10 7,求LIS長度。
我們定義一個B[i]來儲存可能的排序序列,len為LIS長度。我們依次把A[i]有序地放進B[i]里。(為了方便,i的范圍就從1~n表示第i個數)
A[1]=3,把3放進B[1],此時B[1]=3,此時len=1,最小末尾是3
A[2]=1,因為1比3小,所以可以把B[1]中的3替換為1,此時B[1]=1,此時len=1,最小末尾是1
A[3]=2,2大於1,就把2放進B[2]=2,此時B[]={1,2},len=2
同理,A[4]=6,把6放進B[3]=6,B[]={1,2,6},len=3
A[5]=4,4在2和6之間,比6小,可以把B[3]替換為4,B[]={1,2,4},len=3
A[6]=5,B[4]=5,B[]={1,2,4,5},len=4
A[7]=10,B[5]=10,B[]={1,2,4,5,10},len=5
A[8]=7,7在5和10之間,比10小,可以把B[5]替換為7,B[]={1,2,4,5,7},len=5
最終我們得出LIS長度為5。但是,但是!!這里的1 2 4 5 7很明顯並不是正確的最長上升子序列。是的,B序列並不表示最長上升子序列,它只表示相應最長子序列長度的排好序的最小序列。這有什么用呢?我們最后一步7替換10並沒有增加最長子序列的長度,而這一步的意義,在於記錄最小序列,代表了一種“最可能性”。假如后面還有兩個數據8和9,那么B[6]將更新為8,B[7]將更新為9,len就變為7。讀者可以自行體會它的作用。
因為在B中插入的數據是有序的,不需要移動,只需要替換,所以可以用二分查找插入的位置,那么插入n個數的時間復雜度為〇(logn),這樣我們會把這個求LIS長度的算法復雜度降為了〇(nlogn)。話不多說了,show me the code!
1 int put(int arr[], int l, int r, int key)//在arr[l...r]中二分查找插入位置 2 { 3 int mid; 4 if (arr[r] <= key) 5 return r + 1; 6 while (l < r) 7 { 8 mid = l + (r - l) / 2; 9 if (arr[mid] <= key) 10 l = mid + 1; 11 else 12 r = mid; 13 } 14 return l; 15 } 16 17 int LIS(int A[], int n) 18 { 19 int i = 0, len = 1 ,next; 20 int* B = (int *)alloca(sizeof(int) * (n + 1)); 21 B[1] = A[0]; 22 for (i = 1;i < n;i++) 23 { 24 int next = put(B, 1, len, A[i]); 25 B[next] = A[i]; 26 if (len < next) len = next; 27 } 28 return len; 29 }
說了那么多,這個到底有什么用途呢?因為我們新生賽中就有這一題,那就一起來看一下實例吧!
Example:
好多好多球
Time Limit:1000MS Memory Limit:65535K
題型: 編程題 語言: 無限制
描述
一天,Jason買了許多的小球。有n個那么多。他寫完了作業之后就對着這些球發呆,這時候鄰居家的小朋友ion回來了,
Jason無聊之際想到了一個游戲。他把這n個小球從1到n進行標號。然后打亂順序,排成一排。然后讓ion進行一種操作:
每次可以任意選擇一個球,將其放到隊列的最前端或者隊列的最末尾。問至少要進行多少次操作才能使得球的順序變成正序1,2,3,4,5……n。
輸入格式
包含多組測試數據,每組數據第一行輸入一個n(1 <= n <= 100),表示有n個球。第二行有n個數字ai(1 <= ai <= n),ai兩兩各不相同。
輸出格式
每組測試數據輸出占一行,表示最少的操作次數使得小球變得有序。
輸入樣例
4
3 2 1 4
2
2 1
輸出樣例
2
1
分析:題意是把n個亂序的數變為順序,移動次數最少。同樣是用到了最長上升子序列,這里的上升,是連續的、等差的,因為n個球的編號就是從1~n,所以我們找到每次遞增1的最長子序列,剩下的數只要移到隊頭或者隊尾就可以了。那么移動最少次數就等於n-LIS。話不多說,show me the code! 當時是用C來寫的,其實都是一樣的。
1 #include <stdio.h> 2 int main() 3 { 4 int a[150]; 5 int n,i,j,x,count,maxlist; 6 while(scanf("%d",&n)!=EOF) 7 { 8 for(i=0;i<n;i++) 9 scanf("%d",&a[i]); 10 maxlist=1; 11 if(n==1) printf("0\n"); 12 else 13 { 14 for(i=0;i<n;i++) 15 { 16 x=a[i]; 17 count=1; 18 for(j=i+1;j<n;j++) 19 { 20 if(a[j]==x+1) 21 { 22 x++; 23 count++; 24 } 25 } 26 if(count>maxlist) 27 maxlist=count; 28 } 29 printf("%d\n",n-maxlist); 30 } 31 } 32 }
其實當時還不知道最長上升子序列到底是啥東東。。現在學習動態規划就順便復習了一下。也就記錄下來,給自己看看吧。
如有錯誤,敬請指出!
