【洛谷】P1020 导弹拦截/动态规划dp/最长递增子序列/二分优化/树状数组+线段树优化

题目描述

传送门:P1020 导弹拦截

分析:第一问,很明显求最长不递增子序列,dp模板;第二问,则求不递增子序列的最少个数,根据dilworth定理,不递增子序列的最少个数=最长递增子序列的长度。即两问都是LIS问题。

Dilworth定理

若读者有一定离散基础并有兴趣的话,可直接看百度百科狄尔沃斯定理里的归纳法证明。若无离散和相关数学基础但也有兴趣的话,则推荐B站视频:[Dilworth定理] 洛谷 P1020 导弹拦截
以下给出笔者的一些粗陋想法:
每条不递增子序列都满足两个性质:下标递增且数值不递增。
假设已知有k条最长的不递增序列且除起点和终点外没有交点,那么当加入第j个数时(j=k+1),枚举各序列末尾除重合终点外的数x1,x2…xk,若存在xi(1<=i<=k)不大于xj,那么xj就能加入该序列末尾(符合下标递增且数值不递增的性质),若不存在则说明xj必大于xi(i从1到k的所有值),而xj大于xi使最长递增序列的个数+1,此时只能把xj当作一条新的序列(或者说链)的起点,即每条(除重合起点外)链头的下标和数值都能在上一条中找到比它小的,符合递增子序列性质。
结论:
不递增子序列的最少个数=最长递增子序列的长度
不递减子序列的最少个数=最长递减子序列的长度

朴素dp(O(n^2))

定义dp[i]存储以a[i]结尾的最长不递增子序列的长度。可以想到每放一个a[i]就用j遍历0~i-1,若a[j]>=a[i],说明a[i]可放进a[j]后面,此时dp[i] = dp[j] + 1,因题目取最优解,每次遍历取最大值即可。
状态转移方程:dp[i] = max(dp[i],dp[j]+1)

#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; int a[N]; int dp[N];//定义dp[i]=以a[i]结尾的最长不递增序列长度  int cnt; int main(){  while (cin>>a[cnt++]); for (int i=0;i<cnt-1;i++) dp[i] = 1; for (int i=0;i<cnt-1;i++){  for (int j=0;j<i;j++){  if (a[i]<=a[j]) dp[i] = max(dp[i],dp[j]+1); //若符合不递增性质,状态转移 } } int ans = -1; for (int i=0;i<cnt-1;i++){  //printf("dp[%d] = %d\n",i,dp[i]); ans = max(dp[i],ans); //最后答案取最大值 } cout<<ans<<endl; //求最长递增同理 for (int i=0;i<cnt-1;i++) dp[i] = 1; for (int i=0;i<cnt;i++){  for (int j=0;j<i;j++){  if (a[i]>a[j]) dp[i] = max(dp[i],dp[j]+1); } } ans = -1; for (int i=0;i<cnt;i++) ans = max(dp[i],ans); cout<<ans<<endl; } 

二分/O(nlogn)

朴素dp套了双层循环,时间复杂度O(n^2)
而题目数据范围为1e5,O(n^2)显然是不可行的,考虑nlog(n)做法
假若在i之前已经有序列A满足不递增性质,发现a[i]是否加入序列只和序列A末尾值有关,即前面的数不会对当前决策有影响。如果a[i]小于等于末尾值,直接放到末尾,序列长度加一;而如果大于,则通过二分去找第一个比a[i]小的数的位置pos,而这个位置是a[i]在序列的最优位置,因a[i]比a[t]大,那么后来的数接到a[i]后面一定比a[t]的决策更优,所以将a[t]替换成a[i]。

#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 7; int a[N]; int dp[N];//dp[i]用来存当前LIS序列末尾值  int cnt; int main(){  while (cin>>a[++cnt]); dp[1] = a[1]; int len = 1; for (int i=2;i<cnt;i++){ //求最长不递增子序列  if (dp[len]>=a[i]) dp[++len] = a[i];//如果末尾值比a[i]大,直接放入 else { //如果末尾值比a[i]小,查找替换  int pos = upper_bound(dp+1,dp+len+1,a[i],greater<int>()) - dp; //二分查找第一个小于a[i]的位置 dp[pos] = a[i]; } } //for (int i=1;i<cnt;i++) printf("dp[%d] = %d\n",i,dp[i]); cout<<len<<endl; len = 1; memset(dp,0,sizeof(dp)); dp[1] = a[1]; for (int i=2;i<cnt;i++){ //求最长递增子序列  if (dp[len]<a[i]) dp[++len] = a[i];//如果末尾值比a[i]小,直接放入 else { //如果末尾值不比a[i]小,查找替换  int pos = lower_bound(dp+1,dp+len+1,a[i]) - dp; //二分查找第一个大于等于a[i]的位置 dp[pos] = a[i]; } } cout<<len<<endl; } 

桶优化/O(n^2)

取maxi为a[]数组的最大值 ,想象有标号依次为1-maxi的桶,
定义f[j](1<=j<=maxi)为以数i结尾的最长递增序列长度
每当查询a[i]前面有多少数时,遍历前面的1~a[i],取x = max(f[j])+1(1<=j<a[i]),x即为以a[i]结尾的最长子序列长度
同时遍历a[i]+1~maxi的桶,若该桶的值比x小,则
将其赋值为x。
状态转移方程:f[j] = max(f[j],x) (a[i]+1<=j<=maxi)
最长不递增子序列同理

#include <bits/stdc++.h> using namespace std; int cnt,maxi; const int N = 5e4 + 7; const int M = 1e5 + 7; int ans1,ans2; int f[N]; int a[M]; int main(){  while (cin>>a[cnt++]) maxi = max(a[cnt-1],maxi); cnt--; for (int i=0;i<cnt;i++){ //最长不递增  int r = 0; for (int j=a[i];j<=maxi;j++) r = max(r,f[j]); ans1 = max(ans1,r+1); for (int j=a[i];j;j--) f[j] = max(f[j],r+1); } memset(f,0,sizeof(f)); for (int i=0;i<cnt;i++){ //最长递增  int r = 0; for (int j=a[i];j;j--) r = max(r,f[j]); ans2 = max(ans2,r+1); for (int j=a[i]+1;j<=maxi;j++) f[j] = max(f[j],r+1); } cout<<ans1<<endl<<ans2<<endl; } 

时间复杂度依然是O(n^2),发现f[]数组的计数结构可用树状数组维护。

树状数组优化

其本质就是桶优化
只是树状数组提高了查找和更新的效率。

#include <bits/stdc++.h> using namespace std; const int N = 5e4 + 7; const int M = 1e5 + 7; int t[N],a[M]; int cnt,ans1,ans2,maxx; int lowbit(int x){  return x & -x; } int ask(int i){  int maxi = 0; for (;i<=maxx;i+=lowbit(i)) maxi = max(t[i],maxi); return maxi; } void update(int i,int k){  for (;i;i-=lowbit(i)) t[i] = max(t[i],k); } int query(int i){  int maxi = 0; for (;i;i-=lowbit(i)) maxi = max(t[i],maxi); return maxi; } void change(int i,int k){  for (;i<=maxx;i+=lowbit(i)) t[i] = max(t[i],k); } int main(){  while (cin>>a[cnt++]) maxx = max(maxx,a[cnt-1]); cnt--; for (int i=0;i<cnt;i++){ //求最长不递增 int x = ask(a[i]) + 1; ans1 = max(x,ans1); update(a[i],x); } memset(t,0,sizeof(t)); for (int i=0;i<cnt;i++){ //求最长递增 int x = query(a[i]) + 1; ans2 = max(x,ans2); change(a[i]+1,x);//注意这里要a[i]+1 } cout<<ans1<<endl<<ans2<<endl; } 

线段树优化

待补充

全部评论

相关推荐

点赞 评论 收藏
分享
06-13 17:33
门头沟学院 Java
顺序不记了,大致顺序是这样的,有的相同知识点写分开了1.基本数据类型2.基本数据类型和包装类型的区别3.==和equals区别4.ArrayList与LinkedList区别5.hashmap底层原理,put操作时会发生什么6.说出几种树型数据结构7.B树和B+树区别8.jvm加载类机制9.线程池核心参数10.创建线程池的几种方式11.callable与runnable区别12.线程池怎么回收线程13.redis三剑客14.布隆过滤器原理,不要背八股,说说真正使用时遇到了问题没有(我说没有,不知道该怎么回答了)15.堆的内存结构16.自己在写项目时有没有遇见过oom,如何处理,不要背八股,根据真实经验,我说不会17.redis死锁怎么办,watchdog机制如何发现是否锁过期18.如何避免redis红锁19.一个表性别与年龄如何加索引20.自己的项目的QPS怎么测的,有没有真正遇到大数量表21.说一说泛型22.springboot自动装配原理23.springmvc与springboot区别24.aop使用过嘛?动态代理与静态代理区别25.spring循环依赖怎么解决26.你说用过es,es如何分片,怎么存的数据,1000万条数据怎么写入库中27.你说用limit,那么在数据量大之后,如何优化28.rabbitmq如何批次发送,批量读取,答了延迟队列和线程池,都不对29.计网知不知道smtp协议,不知道写了对不对,完全听懵了30.springcloud知道嘛?只是了解反问1.做什么的?短信服务,信息量能到千万级2.对我的建议,基础不错,但是不要只背八股,多去实际开发中理解。面试官人不错,虽然没露脸,但是中间会引导我回答问题,不会的也只是说对我要求没那么高。面完问我在济宁生活有没有困难,最快什么时候到,让人事给我聊薪资了。下午人事打电话,问我27届的会不会跑路,还在想办法如何使我不跑路,不想扣我薪资等。之后我再联系吧,还挺想去的😭,我真不跑路哥😢附一张河科大幽默大专图,科大就是大专罢了
查看30道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务