最长上升子序列&&最长不下降子序列
百练2757:
题目描述:
对于给定的序列,求出最长上升子序列的长度。
题目链接:http://bailian.openjudge.cn/practice/2757
解题思路
一、动态规划
1. 找子问题
错误找法:
“求序列的前n个元素的最长上升子序列的长度”是个子问题,但这样分解子问题,不具有“无后效性” 假设F(n) = x,但可能有多个序列满足F(n) = x。有的 序列的最后一个元素比 an+1小,则加上an+1就能形成更长上 升子序列;有的序列最后一个元素不比an+1小……以后的事 情受如何达到状态n的影响,不符合“无后效性”。
正确找法:
“求以ak(k=1, 2, 3…N)为终点的最长上升子序列的 长度” 一个上升子序列中最右边的那个数,称为该子序列的 “终点”。 虽然这个子问题和原问题形式上并不完全一样,但 是只要这N个子问题都解决了,那么这N个子问题的解中, 最大的那个就是整个问题的解。
2. 确定状态
子问题只和一个变量– 数字的位置相关。因此序列中数 的位置k 就是“状态”,而状态 k 对应的“值”,就是以ak 做为“终点”的最长上升子序列的长度。状态一共有N个。
3.找出状态转移方程
maxLen (k)表示以ak做为“终点”的 最长上升子序列的长度
那么有:
maxLen (1) = 1
maxLen (k) = max { maxLen (i):1<=i < k 且 ai < ak且 k≠1 } + 1 若找不到这样的i,则maxLen(k) = 1
maxLen(k)的值,就是在ak左边,“终点”数值小于ak ,且长度 最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于 ak的子序列,加上ak后就能形成一个更长的上升子序列。
“人人为我”递推型动归程序
1 #include<iostream> 2 #include<algorithm> 3 #include<cstring> 4 using namespace std; 5 int main() 6 { 7 int a[1010],n; 8 int maxlen[1010]; 9 cin>>n; 10 for(int i=1;i<=n;i++) 11 { 12 cin>>a[i]; 13 maxlen[i]=1; 14 } 15 for(int i=2;i<=n;i++) 16 { 17 for(int j=1;j<i;j++) 18 { 19 if(a[i]>a[j]) 20 { 21 maxlen[i]=max(maxlen[i],maxlen[j]+1); 22 23 } 24 } 25 } 26 cout<<* max_element(maxlen+1,maxlen+1+n)<<endl; 27 return 0; 28 }
二、最长上升子序列 (LIS算法(nlong(n)))
设 A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F [t] = 0(t = 1, 2, …, len(A))。则有动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, …, t - 1, 且A[j] < A[t])。
现在,我们仔细考虑计算F[t]时的情况。假设有两个元素A[x]和A[y],满足
(1)x < y < t
(2)A[x] < A[y] < A[t]
(3)F[x] = F[y]
此时,选择F[x]和选择F[y]都可以得到同样的F[t]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] … A[t-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。
再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[t] = k的所有A[t]中的最小值。设D[k]记录这个值,即D[k] = min{A[t]} (F[t] = k)。
注意到D[]的两个特点:
(1) D[k]的值是在整个计算过程中是单调不下降的。
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < … < D[n]。
利 用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[t]与D[len]。若A [t] > D[len],则将A[t]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = A [t];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[t]。令k = j + 1,则有A [t] <= D[k],将A[t]接在D[j]后将得到一个更长的上升子序列,更新D[k] = A[t]。最后,len即为所要求的最长上 升子序列的长度。
在 上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的 时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法 的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<iostream> 5 using namespace std; 6 7 const int mx=100005; 8 int d[mx]; 9 int main() 10 { 11 int n, T; 12 while (scanf("%d",&n)!=EOF) 13 { 14 memset(d, 0, sizeof(d)); 15 int len = 0, x; 16 for (int i=0;i<n;i++) 17 { 18 scanf("%d", &x); 19 if(i == 0)d[++len] = x; 20 else 21 { 22 if(x > d[len])d[++len] = x;///如果是不下降,这里改为>= 23 else{ 24 int pos = lower_bound(d + 1, d + len, x) - d;///如果是不下降,这里改为upper_bound(d + 1, d + len, x) - d; 25 d[pos] = x; 26 } 27 } 28 } 29 printf("%d\n",len); 30 } 31 }