题解 P1020 【导弹拦截】

w1049

2019-02-02 23:09:04

Solution

做这道题的时候看了很多题解,也没看懂,各种查终于弄明白了O(nlogn)的方法,自己来写一篇,试试能不能让更多人知道QwQ 注:树状数组做法请去别的大佬那里看,树状数组还是挺重要的。 这道题的做法很多,别的dalao题解里都有 dalao们也说了,根据"xxxx定理",这题只需要求一个**不上升序列长度**和一个**上升序列长度** 我只说说如何找出它们的长度 写给萌新看,求dalao们轻喷(>﹏<) (如果有锅请dalao们指出) ## 一、lower_bound与upper_bound zhx曾经曰过,STL很慢 hja曾经曰过,觉得STL慢是以zhx为首的一批oi选手的偏见 我们不管他们曰过什么,只来看看这两个函数 ### 1.作用 这两个是STL中的函数,作用很相似: 假设我们查找x,那么: lower_bound会找出序列中第一个**大于等于**x的数 upper_bound会找出序列中第一个**大于**x的数 没错这俩就差个**等于**号╮(╯▽╰)╭ ### 2.用法 以下都以lower_bound做栗子 (因为upper_bound做出的栗子不好吃) (~~其实就是我懒得打两遍~~) 它们俩使用的前提是一样的:序列是**有序**的 对于一个数组a,在[1,n]的区间内查找**大于等于**x的数(假设那个数是y),函数就写成: ```cpp lower_bound(a + 1, a + 1 + n, x); ``` 函数返回一个指向y的指针 看着是不是很熟悉?回想`sort`使用的时候: ```cpp sort(a, a + 1 + n, cmp); ``` 这里`a+1,a+1+n`的写法是不是很像? STL里面的函数写区间一般都这个尿性 同样的,`lower_bound`和`upper_bound`也是可以加**比较函数**`cmp`的: ```cpp lower_bound(a + 1, a + 1 + n, x, cmp); ``` 到这里不得不说说前面的"**有序**序列",这里的"**有序**"是对什么有序? 你可能已经猜到了,它是对于比较器有序,并且必须是**升序**! (为什么不是降序?这个你可能要去问问写STL的人) 一旦对降序序列使用`lower_bound`,就会出现神奇的错误,具体原因可以看这篇: https://blog.csdn.net/qq1337715208/article/details/81072709 当然比较器默认也是"**<**" 如果要在一个**下降**序列里寻找一个**小于**x的数呢? 根据我们之前说的,`lower_bound`只能对上升序列使用,那我假装**下降序列**是个**上升序列**就行了嘛~ (lower_bound:你当我傻吗)(w1049:没错我就当你傻) 只需要把比较器改成"**>**": ```cpp lower_bound(a + 1, a + 1 + n, x, cmp); ``` 同时需要写一个函数`cmp`: ```cpp bool cmp(const int& a,const int& b){return a > b;} ``` 当然,你也可以这样: ```cpp lower_bound(a + 1, a + 1 + n, x, greater <int> () ); ``` 这里的**greater<int>()**就是c++友情提供的方便的大于函数,这样就不用自己动手写一个cmp函数了(~~其实就是懒~~) 它们的实现方式是二分查找 ,存在的意义就是让我们写代码更方便~~地偷懒~~(一行lower_bound比写二分查找方便多了) ### 3.返回值 众所周知,~~小葱非常擅长计算组合数~~返回的是个**指针** 对于返回值我们有两种处理方式: ###### 第一种: 许多人讨厌指针,那么我们用这个指针**减去**数组开头的指针(即**数组名**),得到两个指针的差,就是**数组下标**,即: ```cpp int p = lower_bound(懒得写) - a; ``` 那么a[p]就是要找的y (如果不知道为什么就记着好了) ##### 第二种: 指针好!指针秒! 改革春风吹满地,用指针的oier真争气! (以上两行你可以当做什么都没看见) ```cpp int *p = lower_bound(还是懒得写); ``` 那么*p就是要找的y ~~可以看出指针多么直接,不像数组下标还要倒腾一遍~~ #### 总结一下:#### 好像没什么可总结的QwQ 对一个**下降**序列a ```cpp int p = lower_bound(a + 1, a + 1 + n, x, greater <int> () ) - a; ``` a[p]即a[1]到a[n]中第一个小于等于x的数 (被遗忘的upper_bound表示不服) ## 二、O(nlogn)求出最长不上升子序列的长度 (即**一套系统最多拦截数**)(~~终于到二了~~) ### 1.实现方式 首先我们需要一个数组**a**,存储从第1个到第n个导弹的高度 然后一个数组**d**(其实是个栈),存储不上升序列 把a中的**每个元素**挨个加到d里面: (a中第i个元素为a[i],d长度为len,d中最后一个(也是最小的一个)为d[len]) #### 如果a[i] <= d[len],说明a[i]可以接在d后面(而整个d还是有序的),那就简单粗暴地把a[i]丟进d: ```cpp d[++len] = a[i] ``` #### 如果a[i] > d[len],说明a[i]接不上 但是我们发扬**瞎搞精神**:**接的上要接,接不上创造条件也要接!** ###### 强行把a[i]塞进去: 在d中找到**第一个小于**a[i]的数,把它**踹了**,用a[i]**代替**它!(为什么正确在下面) 假设这个数是y,怎样踹掉它呢? 很明显,我们需要使用lower_bound和upper_bound来查找 **第一步,找一个听起来无比正确的理由**,比如它占着位置不干活啦,干起活来还不如a[i]啦,naive啦,它too young啦,too simple啦......反正能骗过lower_bound和upper_bound就行 (lower_bound&&upper_bound:你当我们傻)(w1049:真聪明) 接下来,特别有正义感的lower_bound和upper_bound就会去把y给拎出来 **第二步,考虑使用什么** 我们知道,要求的是最大**不上升**子序列长度,也就是如果两个元素**相等也是可以**的 所以我们踹人就**不用踹等于a[i]的**了 结合上面,应该使用**upper_bound**(终于想起来它了)并且**使用>作为比较器**(这是个下降序列) **第三步,直接开搞** ```cpp int p = upper_bound(d + 1, d + 1 + len, a[i], greater<int>()) - d; d[p] = a[i]; ``` 成功把a[i]塞了进去 ### 2.为什么正确 ~~显然成立~~ 如果y**在**末尾,由于y < a[i],所以**y后面能接的不如a[i]多**,y让位给a[i]可以让序列更长 如果y**不在**末尾,那y有生之年都**不会再被用到**了,直接踹了y就行,y咋样,**who care?** 注意到lower_bound只能在**有序**序列中使用,此时d还有序吗? **当然有序**。(本文第一个句号) 假设y前一个y1,y后一个是y2,则 $y1 > y > y2$ 因为y是第一个小于a[i]的,所以 $y1 > a[i]$ 又因为 $a[i] > y > y2$ 所以 $y1 > $**a[i]**$ > y2$ 对比下原来的式子 $y1 > $**y**$ > y2$ a[i]可以完美代替y,至于y以后咋办,**who care?** 对于**最长上升子序列**,只需要把上面的过程通通**换一下符号** 可以用以下方法证明: ```cpp 反之亦然同理,推论自然成立,略去过程QED,由上可知证毕(多么美妙的证明) ``` 实际上,$d[i]$的含义是:最大不上升子序列长度为$i$时,最优的结尾元素。 ### 3.代码: ```cpp for(int i=2;i<=n;i++) if(d[len]>=a[i])d[++len]=a[i]; else { int p=upper_bound(d+1,d+1+len,a[i],greater<int>())-d; d[p]=a[i]; } ``` 最后len就是要求的最大不上升子序列**长度** 但要注意的是,d中存储的**并不是**最大不上升子序列! 原因如下: ```cpp 即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难 ``` ### 4.对样例模拟: ~~在这里推荐一下DevC++的调试器(不用DevC++的当我没说)~~ (还是不要推荐了) 1.我们把a[i]**(389)**加入d: ![1](https://cdn.luogu.com.cn/upload/pic/50859.png) 2.**i=2**,此时a[i]**(207)**<=d[len]**(389)**,把a[2]加入d: ![2](https://cdn.luogu.com.cn/upload/pic/50860.png) 3.**i=3**,此时a[i]**(155)**<=d[len]**(207)**,把a[3]加入d: ![3](https://cdn.luogu.com.cn/upload/pic/50861.png) 4.**i=4**,此时a[i]**(300)**>d[len]**(155)**,不能直接加入,所以准备踹人 ![4](https://cdn.luogu.com.cn/upload/pic/50863.png) 5.找出d中第一个小于a[i]**(300)**的(即**207**),用a[i]换掉 ![5](https://cdn.luogu.com.cn/upload/pic/50864.png) 6.**i=5**,此时a[i]**(299)**>d[len]**(155)**,不能直接加入,所以准备踹人 ![6](https://cdn.luogu.com.cn/upload/pic/50870.png) 7.找出d中第一个小于a[i]**(299)**的(即**155**),用a[i]换掉 ![7](https://cdn.luogu.com.cn/upload/pic/50865.png) 8.**i=6**,此时a[i]**(170)**<=d[len]**(299)**,把a[6]加入d: ![8](https://cdn.luogu.com.cn/upload/pic/50866.png) 9.**i=7**,此时a[i]**(158)**<=d[len]**(170)**,把a[7]加入d: ![9](https://cdn.luogu.com.cn/upload/pic/50868.png) 10.**i=8**,此时a[i]**(65)**<=d[len]**(158)**,把a[8]加入d: ![10](https://cdn.luogu.com.cn/upload/pic/50869.png) **至此**,得到最大不上升子序列长度**len=6** ## 三、AC代码: ```cpp #include <iostream> #include <algorithm> using namespace std; const int N = 100010; int a[N], d1[N], d2[N], n; int main() { while (cin >> a[++n]); n--; //输入 int len1 = 1, len2 = 1; //初始长度为1 d1[1] = a[1]; //用于求不上升序列长度 d2[1] = a[1]; //用于求上升序列长度 for (int i=2; i<=n; i++) { //从a[2]开始枚举每个数(a[1]已经加进去了) if (d1[len1] >= a[i]) d1[++len1] = a[i]; //如果满足要求(不上升)就加入d1 else { //否则用a[i]替换d1中的一个数 int p1 = upper_bound(d1 + 1, d1 + 1 + len1, a[i], greater<int>()) - d1; d1[p1] = a[i]; } if (d2[len2] < a[i]) d2[++len2] = a[i]; //同上 else { int p2 = lower_bound(d2 + 1, d2 + 1 + len2, a[i]) - d2; d2[p2] = a[i]; } } cout << len1 << endl << len2; //输出 return 0; //结束 } ``` 更快速的版本: ```cpp #include<iostream> #include<cstdio> #include<algorithm> #define R register using namespace std; const int N=100010; int a[N],d1[N],d2[N],n; inline bool read(int &x) { char c=getchar(); if(c==EOF)return false; while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9') { x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return true; } int main() { while(read(a[++n]));n--; R int len1=1,len2=1; d1[1]=d2[1]=a[1]; for(R int i=2; i<=n; i++) { if(d1[len1]>=a[i])d1[++len1]=a[i]; else *upper_bound(d1+1,d1+1+len1,a[i],greater<int>())=a[i]; if(d2[len2]<a[i])d2[++len2]=a[i]; else *lower_bound(d2+1,d2+1+len2,a[i])=a[i]; } printf("%d\n%d",len1,len2); return 0; } ``` 一年过去,百感交集,虽然已经凉了,但是还是改正了一下这篇我唯一写的还行的题解里面的错误,祝各位在复活的NOIP中取得好成绩。 原来的代码`return 0;`中有个中文分号,现在已经没了。