题目:
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入输出样例
输入 #1
389 207 155 300 299 170 158 65
输出 #1
6 2
正文
这道题的题目大概可以看作是:给定一个正整数序列S,求S的最长不上升子序列和S的不上升子序列个数最少是多少
第一问
方法一:DP
其中一种方法是使用动态规划,大致思路是设dp[i]表示以i为结尾的子序列的最长不上升子序列长度,dp的数据类型如下:
struct node{
int p;//prevent
int non_p;//do not prevent
};
其中p表示拦截该枚导弹的最长下降子序列的值,non_p表示不拦截该枚导弹的最长下降子序列的值
在遍历到s[i]时将s[0]~s[i-1]的子序列遍历一遍(建议倒序遍历),寻找比s[i]大的值设其下标为n,一旦找到,则可得:
dp[i].non_p=max(dp[i-1].p,dp[i-1].non_p);
dp[i].p=dp[n].p+1;
最后结果应该就是max(dp[i].p,dp[i].non_p)
时间复杂度O()
有不正确之处欢迎指出!
方法二:贪心+二分
首先,定义vector<int> tmp,初始化为空vector,再定义一个变量tmin=∞用来表示tmp当中最小的值,随后从s[0]开始遍历序列,接下来会遇到:
1,s[i]<tmin,即s[i]比tmp的所有元素都要小(这就是为什么初始化tmin=正无穷的原因),这时将s[i]加入tmp,并将tmin设为s[i],这个就是贪心思维,应该很好理解:如果可以多拦截一个导弹,就多拦一个
vector<int> tmp;
int tmin=1145141919;
for(……){
if(s[i]<tmin){
tmp.push_back(num);
tmin=num;
}else{
……//第二种情况
}
}
2,s[i]>=tmin时,在tmp中查找比s[i]大的值,设下标为s1,比s[i]小(或等于)的值,设下标为s2,将tmp[s2]的替换为s[i]。最终tmp.size()就是答案,查找可用二分将时间复杂度降至O()
值得注意的是,tmp序列不是该序列的最长上升子序列,只是长度一致的子集
int l=0,r=tmp.size()-1,mid=0;
for(……){
if(s[i]<tmin){
……//第一种情况
}else{
while(l<r){
mid=(l+r)/2;
if(tmp[mid]>=num){
l=mid+1;
}else{
r=mid;
}
}
tmin=tmp.back();
}
}
这可能令人费解,我们可以假设序列Sl为S的最长下降子序列,将Sl当中某些值用S中的某些元素代替,并保证进行完这一步后Sl仍然是一个下降序列,上述操作就是创造出了一个经过改动的Sl(tmp),长度仍然是最长下降子序列的长度,但是这个序列并不是正确的,因为经过了改动本人想了一整个晚上才想明白 。
至于为什么是创造了一个经过改动的sl,这里借用洛谷上的题解(由Micnation_AFO撰写)进行解释(蒻弱求助):
“令 f_i 为长度为 i的不上升子序列的末尾元素,我们可以发现 f 是单调非严格递减的,可以反证法:若存在 i < j,且 f_i < f_j,那么一定有一个长度为 j 的不上升子序列。由于 i < j,那么该序列的末尾 i项就是长度为 i 不上升子序列,并且这个序列的末尾一定是 f_i,而这个长度为 j 的不上升子序列的末尾也是 f_j,所以 f_i < f_j 并不成立,即 f是单调非严格递减的。”
从这里看出,经过改动后的Sl其实是由多个S的不上升子序列颠倒了顺序而成,所以改动后的Sl长度是不变的,将各个子序列排回去就可以了
将值替换为s[i],可以理解为原先只能选择S当中属于(0,tmp[s2]]的元素用于替换tmp[s2]之后的值,即拦截的导弹,现在变成了(0,s[i]],已知tmp[s2]<s[i],因此后一个区间范围更大,s[i]之后的数字(s[i+1~s.end])∈(0,s[i]]的概率就越大,也就会有尽可能多的数是第一种情况,tmp的长度就越大。
而替换tmp[s2]而不是tmp[s1],替换了[s1]之后区间由(0,tmp[s1]]变为(0,s[i]],tmp[s1]>s[i],后一个范围小,(s[i+1~s.end])∈(0,s[i]]概率降低,故不替换
时间复杂度O()
(此外本小题貌似还可以用Dilworth定理进行解释,本人蒻弱就不多说了,以免误导)
第二问
贪心,设vector<int> xt代表每一套系统能拦截的最大高度,xt.size()就是需要配套的系统数,初始时将s[0]项加入xt(因为肯定至少有一套系统),随后从s[1]开始遍历整个S, 在遍历到s[i]项时在xt内寻找哪一套能够拦截这颗导弹,也就是遍历xt,寻找是否存在xt[j],使得xt[j]>=s[i]。若找到则令xt[j]=s[i]并跳出循环,若找不到(不存在xt[j]使得xt[j]>=s[i])则将s[i]加入xt,最后xt.size()即为答案
vector<int> xt;
xt.push_back(s[0]);
for(int i=1;……){
bool isjoin=false;//是否找到
for(int j=0;j<xt.size();j++){
if(s[i]<=xt[j]){
xt[j]=s[i];
isjoin=true;//找到力!
break;
}
}
if(!isjoin){
xt.push_back(s[i]);//没找到(不存在)
}
}
cout<<xt.size();
可以这么理解:能拦截就拦截,拦不了就新增一台;每一套系统都要尽可能多的拦截的导弹,所以一旦xt中有系统可用于拦截当前导弹就会把这颗导弹拦截了。同时,拦截了这颗导弹对后续的导弹拦截并没有影响(若后面有些导弹无法被这台系统拦截,可以用另一台拦截,若所有系统都无法拦截则多开一个系统),可用大致理解为无后效性。
时间复杂度O()(貌似luogu的数据受潮了,这个算法可以拿到200分)
(优化:事实上,个人认为vector<int> xt可用改用STL容器set,set会自动排序,因此每次插入的时间复杂度是O(),寻找合适的xt[j]可用二分法,时间复杂度O(
),这样遍历到s[i]项时,需要进行的操作的时间复杂度就是O(
),这个小问总的时间复杂度可以优化为O(
),但本人秉承着少做少错
(懒)的原则就没去做,感兴趣的话可尝试)
总结
整个大题总的时间复杂度大概是O()(自己写的代码,若第二小问进行了优化可以缩减到O(
)),实测可以在洛谷上拿到满分
下面是全部AC代码:
//daodan数组即为文中提到的S,其余命名均与文中一致
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
int daodan[500005];
vector<int> tmp;
vector<int> xt;
int tmin=1145141919;
int n=0,ans2=1;
void midfind(int num){
int l=0,r=tmp.size()-1,mid=0;
if(num<tmin){//若s[i]<tmin
tmp.push_back(num);
tmin=num;
return;
}
while(l<r){//二分
mid=(l+r)/2;
if(tmp[mid]>=num){
l=mid+1;
}else{
r=mid;
}
}
tmin=tmp.back();//刷新tmin
if(num<=tmin){//(?)用于应对tmp当中只有一个元素的特殊情况
tmp.push_back(num);
tmin=num;
return;
}
tmp[l]=num;//替换
}
int main(){
while(cin>>daodan[n]){
n++;
if(getchar()=='\n')break;
}
//第一问
for(int i=0;i<n;i++){
midfind(daodan[i]);
}
cout<<tmp.size()<<'\n';
//第二问
xt.push_back(daodan[0]);
for(int i=1;i<n;i++){
bool isjoin=false;
for(int j=0;j<xt.size();j++){
if(daodan[i]<=xt[j]){
xt[j]=daodan[i];
isjoin=true;
break;
}
}
if(!isjoin){
xt.push_back(daodan[i]);
}
}
cout<<xt.size();
//END
return 0;
}