这是一道经典的最长上升子序列的题目--拦截导弹
题目给出了两个问题:
第一个是问最多能够拦截的导弹数量,即最长不上升子序列
第二个是问该数组最少能够被多少个最长不上升子序列全部覆盖
问题一:
关于最长不上升子序列的求解,只需用动态规划可以快速解决,用h[i]表示每个导弹的高度,用一个状态方程f[i]表示以第i个导弹为最后一个点的最长不上升子序列的长度,那么我们只要开始时将每个f[i]初始化为1(即本身为一个子序列),然后找出j<i中h[j]>=h[i]的点那么f[i]的状态转移方程可以写为max(f[i],f[j]+1),接下来比较所有的f值,输出最大值即可。
上问题一核心代码:
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<i;j++){
if(h[i]<=h[j]) f[i]=max(f[i],f[j]+1); //状态转移方程
}
res=max(res,f[i]); //比较每个点的f值
}
问题二:
关于数组最少能够被多少个最长不上升子序列全部覆盖的问题,这里需要用到贪心和二分。
我们可以设定一个数组存储所有的最长不上升子序列,为使得该数组元素数量最少,我们应该要将新的h值尽量插入到已有的子序列中,实在不行了再创造新的子序列。那么如何才能将h值最大化地插入到已有的子序列中呢,对于每个可插入的子序列,由于题目只涉及到其最后一个元素以及总子序列个数(划重点),所以我们可以猜想如果将该h值插入到最后一个元素最接近h值的子序列中才可以最大化节省空间,即使得总子序列个数最少。
猜想存在,开始证明贪心:还是看那个重点--由于题目只涉及到其最后一个元素以及总子序列个数,所以存在情况使得交换两个子序列的从中间开始某一点的子序列是不会改变总子序列个数(举个例子:
因为最优解本质是正确答案下的不同情况,而我们的贪心解是在用一种情况逼近答案,结合上述结论,我们可以知道最优解可以通过上述转变从而变成我们的贪心解,即最优解>=贪心解
然后因为最优解所求出的总子序列个数一定是最少的,而我们的贪心解所求出来的不可能逼最优解还要小所有又有如下结论:最优解<=贪心解
根据两个结论,可以得出最优解=贪心解,至此贪心证明完毕
优化:还是那个重点--由于题目只涉及到其最后一个元素以及总子序列个数,所以我们不必要设定一个数组存储所有的最长不上升子序列,只需要存储所有的最长不上升子序列的最后一个元素,我们用一个单调不下降的数组tt来存储。然后当h值小于tt[i]时,即可用贪心加二分来找到贪心解存入,当h值大于所有tt时,就多开一个子序列来存储。
上问题二核心代码:
for(int i=1;i<=n;i++){
int l=0,r=cnt;
while(l<r){ //二分法找贪心解
int mid=l+r>>1;
if(tt[mid]>=h[i]) r=mid;
else l=mid+1;
}
if(tt[r]<h[i]) r++; //如果此时无贪心解,那么r++,使得r成为新的子序列上限cnt
cnt=max(cnt,r); //如果上面的if运行,即无贪心解,那么更新cnt来创造新的子序列
tt[r]=h[i]; //子序列的末尾元素的更新
}
至此,两个问题都已经解决,放一下总代码:
#include<iostream>
using namespace std;
const int N=1010,INF=31000;
int h[N],tt[N],f[N];
int n;
int main(){
int a,res=0,cnt=0;
while(cin>>a) h[++n]=a; //对于题目的输入方式,应该这样读入
for(int i=1;i<=n;i++){
f[i]=1;
for(int j=1;j<i;j++){
if(h[i]<=h[j]) f[i]=max(f[i],f[j]+1); //状态转移方程
}
res=max(res,f[i]); //比较每个点的f值
}
cout<<res<<endl;
for(int i=1;i<=n;i++){
int l=0,r=cnt;
while(l<r){ //二分法找贪心解
int mid=l+r>>1;
if(tt[mid]>=h[i]) r=mid;
else l=mid+1;
}
if(tt[r]<h[i]) r++; //如果此时无贪心解,那么r++,使得r成为新的子序列上限cnt
cnt=max(cnt,r); //如果上面的if运行,即无贪心解,那么更新cnt来创造新的子序列
tt[r]=h[i]; //子序列的末尾元素的更新
}
cout<<cnt<<endl;
return 0;
}