2015 UESTC Training for Dynamic Programming N - 导弹拦截(LIS (nlogn))
N - 导弹拦截
Time Limit: 3000/1000MS (Java/Others) Memory Limit: 65535/65535KB (Java/Others)
某国为了防御敌国的导弹袭击,开发出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都要高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭,并观测到导弹依次飞来的高度,请计算这套系统最多能拦截多少导弹,同时,司令部想知道拦截下来的导弹的高度。拦截来袭导弹时,必须按来袭导弹袭击的时间顺序,不允许先拦截后面的导弹,再拦截前面的导弹。
Input
第一行是一个整数t,代表case数。 对于每一个case,第一行是一个整数n(1≤n≤100000); 第二行是n个非负整数,表示第n枚导弹的高度,按来袭导弹的袭击时间顺序给出,以空格分隔。数据保证高度不会超过100000.
Output
对于每一个case,第一行输出最多能拦截的导弹数,第二行按来袭顺序输出拦截下来的导弹的高度构成的序列,以一个空格隔开。若有不止一种方法可以拦截最多的导弹,输出字典序最小的。
Sample input and output
Sample Input | Sample Output |
---|---|
1 5 1 6 3 5 7 |
4 1 3 5 7 |
解题思路:
这道题一看,我的天,这不是NOIP的题吗。。。仔细一看数据范围加大了,而且还多了输出的要求,就是让你以字典序最小的形式输出这个最长上升子序列.
好了,先来说说有关求LIS的方法吧,LIS顾名思义,就是我们所说的 longest increasement sequence 啦,比如在一个序列中,1 2 3 4 5 1 那么这个序列的LIS就是5了。关于求LIS有两种常用的算法,一个是O(n2)的,一个是O(nlogn)的,两者的区别就在于寻找a[j]的方式不同,一个是暴力找的,一个是用二分来找的。。
先来说说O(n2)的做法吧。
首先,我们对于知道。对于一个序列,如果我想求出他的最长上升子序列的话,我们要做的事情很简单,每次枚举i的位置,然后从i的后面开始往后找使得a[j]>a[i].
在这里,我们定义一个状态dp[i]表示的是a[i]结尾的最长上升子序列的长度了。我们一开始使得所有的dp[i]=1,就是说在没有枚举前,所有的单个子序列都是以自己为LIS的,下来我们只需要一次一次的更新答案就好了。
然后是O(nlogn)的做法,为了能够在寻找a[j]>a[i]时更加方便和快捷,我们采用了二分的方法。。
先定义一个数组m[x],表示的是长度为x的上升子序列以m[x]作为最小末尾,比如所m[3]=17说的就是一个长度为3的子序列的末尾元素是17,为什么说是最小的末尾呢?后面会讲到。
由于是求LIS,顾名思义,m[]数组中的每个元素都是上升的,这样一来,如果x<y,那么m[x]<m[y],结合二分和这个性质就能够很快捷的进行各种查找了。
比如说,我们假定m[]存放的目前来说最长上升子序列的长度是k,那么当我们枚举到第i个数字时,如果a[i] > m[k],则m[++k] = a[i];
否则的话,我们就在[1,k]中利用二分去找一个pos,使得pos具有这样的性质,小于等于a[i]的最大位置,m[p] = a[i];
比如说现在已经知道了k=6,m[6]=17, 1 2 3 11 16 17,此时的a[i] = 7的话,因为a[i]=7<m[k],所以m[3] = 7。看懂了吗?就是说,在一个长度为3的上升子序列中,他的最小末尾是7.
代码(n^2的LIS求法):
1 # include<cstdio> 2 # include<iostream> 3 4 using namespace std; 5 6 # define MAX 1234 7 8 int a[MAX]; 9 int dp[MAX]; 10 11 int main(void) 12 { 13 int n;scanf("%d",&n); 14 for ( int i = 0;i < n;i++ ) 15 { 16 scanf("%d",&a[i]); 17 dp[i] = 1; 18 } 19 int ans = -1; 20 for ( int i = 0;i < n;i++ ) 21 { 22 for ( int j = i+1;j < n;j++ ) 23 { 24 if ( a[j] > a[i] ) 25 { 26 dp[j] = max(dp[j],dp[i]+1); 27 ans = max(ans,dp[j]); 28 } 29 } 30 } 31 printf("%d\n",ans); 32 33 34 return 0; 35 }
代码(nlogn的LIS求法):
1 # include<cstdio> 2 # include<iostream> 3 # include<cstring> 4 5 using namespace std; 6 7 # define MAX 100000+4 8 9 int a[MAX]; 10 int m[MAX]; 11 int pre[MAX]; 12 13 int bsearch( int * m,int n,int t ) 14 { 15 int lo = 1, hi = n; 16 while ( lo <= hi ) 17 { 18 int mid = ( lo+hi )>>1; 19 if ( a[mid]==t ) 20 { 21 return mid; 22 } 23 else if ( a[mid] < t ) 24 { 25 lo = mid+1; 26 } 27 else 28 { 29 hi = mid-1; 30 } 31 } 32 return lo; 33 } 34 35 int main(void) 36 { 37 int t;scanf("%d",&t); 38 while ( t-- ) 39 { 40 int n;scanf("%d",&n); 41 for ( int i = 1;i <= n;i++ ) 42 { 43 scanf("%d",&a[i]); 44 } 45 int maxlen = 1; 46 m[maxlen] = a[1]; 47 // pre[maxlen] = 1; 48 49 for ( int i = 2;i <= n;i++ ) 50 { 51 if ( a[i] > m[maxlen] ) 52 { 53 m[++maxlen] = a[i]; 54 // pre[maxlen] = i; 55 } 56 else 57 { 58 int p = bsearch(m,maxlen,a[i]); 59 m[p] = a[i]; 60 // pre[i] = p; 61 } 62 } 63 64 //printf("%d\n",maxlen); 65 //printf("%d",a[pre[1]]); 66 // for ( int i = 2;i <= maxlen;i++ ) 67 // { 68 // printf(" %d",a[pre[i]]); 69 // } 70 printf("\n"); 71 memset(pre,0,sizeof(pre)); 72 memset(m,0,sizeof(m)); 73 } 74 75 76 return 0; 77 }
那么就针对这道题目来说,题目让我们求出的就是最长上升子序列的个数而且还要求我们以字典序最小的形式输出,,,