AcWing 1010 拦截导弹

题目描述:

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

共一行,输入导弹依次飞来的高度。

输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

分析:

本题是练习动态规划,贪心以及二分的好题,下面我将分别用这三种方法求解本题。题目让我们求解两个数量,第一个是最长不上升子序列的长度,第二个是序列最少可以划分为几个不上升子序列。

方法一:动态规划

首先求解最长不上升子序列的长度,f[i]表示以a[i]为末尾的最长不上升子序列的长度,状态转移方程为f[i] = max(f[i],f[j] + 1),其中j < i,a[i] <= a[j]。第二个问题求拦截所有导弹需要配备多少个系统,稍微复杂些。用DP解决需要用到Dilworth定理。对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真,它断言:对于任意有限偏序集,其最长链中元素的数目必等于其最小反链划分中反链的数目。应用到本题就是说把一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度。故第二个问题就转化为了求LIS的长度,g[i] = max(g[i],g[j] + 1),i > j,a[i] > a[j]。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int f[N],a[N],b[N];
int main(){
    int n = 0;
    while(cin>>a[n])   n++;
    int res = 0;
    for(int i = 0;i < n;i++){
        f[i] = 1;
        for(int j = 0;j < i;j++){
            if(a[i] <= a[j])    f[i] = max(f[i],f[j]+1);
        }
        res = max(res,f[i]);
    }
    int res1 = 0;
    for(int i = 0;i < n;i++){
        b[i] = 1;
        for(int j = 0;j < i;j++){
            if(a[i] > a[j]) b[i] = max(b[i],b[j]+1);
        }
        res1 = max(res1,b[i]);
    }
    cout<<res<<endl<<res1<<endl;
    return 0;
}

方法二:贪心

AcWing 896 最长上升子序列 II中我们介绍了LIS问题的贪心+二分解法,但是是强行把DP转化为贪心的解释,不容易理解,下面正式从纯贪心的角度来分析这类问题。第一个问题,求最长不上升子序列的长度,在各个长度的不上升子序列中,只有末字符会影响子序列的延展,因此,我们需要存储各个长度不上升子序列的末字符,而且为了后面的字符能够接上不上升序列,前面子序列的末字符越大越好,所以我们需要维持一个数组,始终存储着各个长度的不上升子序列末字符的最大者。举个例子:3 6 5 5 8 2。遍历下该序列,遍历到3时,发现一个长度为1的不上升子序列,因此f[0] = 3,然后遍历到6,6无法接到3后面且大于3,因此长度为1的不上升子序列的末字符替换为f[0] = 6,遍历到5,出现了长度为2的不上升子序列,于是f[1] = 5,接着遍历还是5,655也是不上升序列,故f[2] = 5,然后是8,没有比8大的,故f[0] = 8,之后是2,f[3] = 2.最后的不上升子序列是长度是4。因为我们始终将f数组中小于a[i]的最大者替换为a[i],故f数组总是非上升的。

第二个问题,求序列最少能划分为几个不上升子序列。我们可以维持每个子序列的末元素,然后在各个末元素中找到不小于a[i]的最小的末元素,将a[i]接到末尾。还是以3 6 5 5 8 2为例,第一个序列为3,g[0] = 3,然后是6无法接到3后面,于是第二个子序列末字符是g[1] = 6,然后5可以接到6后面,于是g[1] = 5,接着5还可以接到5后面,于是g[1] = 5,8大于所有末字符,新建第三个子序列f[2] = 8,2可以接到三个子序列任意一个后面,为了让末字符的较大着留到后面,于是把2接到3后面,得到g[0] = 3.最后求得最少可以划分为三个不上升子序列,分别是3 2,6 5 5,8.g数组始终存储着各个子序列的末字符,为什么遍历到2时要接到3后面而不是8后面,因为接到3后面,如果下一个是7,7可以接到8后面从而不用新建序列,但是把2接到8后面的话遍历到7就将必须新建一个不上升子序列了。

可见,求LIS问题用DP容易理解,求最少划分序列数问题用贪心更容易理解,因此如果不理解求最长不上升子序列的贪心解法,可以转化为其反链问题求数列能够划分为的下降子序列的最小个数,就容易理解问题一的贪心思想了。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int a[N],f[N],g[N];
int main(){
    int n = 0,res = 0,len = 0;
    while(cin>>a[n])    n++;
    for(int i = 0;i < n;i++){
        int j = 0;
        while(j < len && f[j] >= a[i])  j++;
        f[j] = a[i];
        len = max(len,j + 1);
    }
    res = len,len = 0;
    for(int i = 0;i < n;i++){
        int j = 0;
        while(j < len && g[j] < a[i])   j++;
        g[j] = a[i];
        len = max(len,j + 1);
    }
    cout<<res<<endl<<len<<endl;
    return 0;
}

方法三:贪心 + 二分

在贪心解法中,需要求f数组中不小于a[i]的最小的数,有f数组是单调非增的,因此完全可以用二分去查找。查找g数组中元素时也可以用二分去查找。

首先求最长不上升子序列长度,需要在f数组(单调非增)中找到最小的不小于a[i]的数,然后用a[i]替换掉其后一个元素。比如6 5 5 3,在其中查找5时,不小于5的最后一个数是5,然后用5替换掉5后面的数3得到6 5 5 5;如果是在6 5 5 3中查找2,则等同于在3的后面插入2。这也是为什么我们不直接查找小于a[i]的第一个位置的原因,因为有可能f数组中所有的数都大于a[i],我们查找不小于a[i]的最大的数,其后面不论有没有数都可以替换为a[i]。这里的二分查找是找到了就替换,找不到就新增,所以我们将目标设定为在单调非增序列中查找不小于a[i]的最小数位置。下面是如何进行二分,f[mid] >= a[i]时,待查找的位置一定不在mid左边,所以l = mid,f[mid] < a[i],则待查找位置一定在mid左边,故r = mid - 1。我们最终的目标是l = r 且他们的指向的元素都是不小于a[i]的,所以遇见小于a[i]的,r需要指向mid - 1。另外,避免二分死循环的关键在于l和r指针是否相对于mid移动了。当r = l + 1时,如果mid = l + r >> 1,mid = l,此时若满足l = mid的条件将永远死循环,所以只有l = mid + 1时才可避免死循环。当r = l + 1时,如果mid = l + r + 1 >> 1,mid = r,此时若满足r = mid - 1的条件,r会回退一格,使l与r相遇,循环终止。所以我们这里mid取l + r + 1 >> 1。这是二分的一条特别重要的经验,即以l == r作为二分循环的终止条件时,如果mid = l + r >> 1,即mid取(l + r) / 2下取整,此时l必须转移到mid + 1,否则会造成死循环;如果mid = l + r + 1 >> 1,即mid取(l + r) / 2上取整,此时r必须转移到mid - 1,否则会造成死循环。很多人不知道二分出现死循环的原因,只要记住这一条便不会出现死循环。

正如上面的步骤,二分程序设计的思路需要首先明确查找的目标位置,设计出循环退出时l与r的位置,设计出mid的取值以及满足何条件时,l与r是否要相对于mid移动。下面用这个步骤来解决第二个问题,求数列最少能够被划分为多少个不上升子序列。只有出现大于已存在所有子序列末尾的元素才会新建子序列,所以g数组是严格单调递增的(之前的f是单调非增的)。我们要在g中找到不小于a[i]的最小值,等价的转化为找到小于a[i]的最大值,后面一个元素即是所求位置,即最终l指向小于a[i]的最大值。g[mid] < a[i]时,待查找位置一定不在mid左边,故l = mid;g[dmid] >= a[i]时,待查找位置一定在mid左边,故r = mid - 1.r的位置相对于mid发生了位移,因此,mid = l + r + 1 >> 1。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int a[N],f[N],g[N];
int main(){
    int n = 0,res = 0;
    while(cin>>a[n])    n++;
    int len = 0,l,r;
    for(int i = 0;i < n;i++){
        l = 0,r = len;
        while(l < r){
            int mid = l + r + 1>> 1;
            if(f[mid] >= a[i])  l = mid;
            else    r = mid - 1;
        }
        f[l + 1] = a[i];
        len = max(len,l + 1);
    }
    res = len,len = 0;
    for(int i = 0;i < n;i++){
        l = 0,r = len;
        while(l < r){
            int mid = l + r + 1>> 1;
            if(g[mid] < a[i])  l = mid;
            else    r = mid - 1;
        }
        g[l + 1] = a[i];
        len = max(len,l + 1);
    }
    cout<<res<<endl<<len<<endl;
    return 0;
}
发布了272 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/104059659