导弹防御系统

导弹防御系统

题目描述

在这里插入图片描述


题意解释

题目想要求的是最长上升子序列的个数+最长下降子序列的个数。但是并不是说独立地先求出最长上升子序列的个数,再求出最长下降子序列的个数,最后再相加得到总和。因为每一个数都有两种选择,要么属于最长上升子序列,要么属于最长下降子序列。因此这题不能孤立地求,而是要依次枚举每一个数的情况。


核心思路

为了能遍历所有情况,我们首先考虑搜索顺序是什么。

搜索顺序分为两个阶段:

  • 从前往后枚举每颗导弹属于某个上升子序列,还是下降子序列;即依次枚举每个数,每个数都有两种可能的选择,要么是某个上升子序列,要么是某个下降子序列。
  • 如果属于上升子序列,则枚举属于哪个上升子序列(包括新开一个上升子序列);如果属于下降子序列,可以类似处理。即如果该数被放到了单调上升的序列中,则枚举将该数放到哪个单调上升的序列后面。如果该数被放到了单调下降的序列中,则枚举将该数放到哪个单调下降的序列后面。或者该数成为一个单独的单调序列

up[] 存储当前所有上升子序列的末尾元素,而且要注意up数组它其实是单调递减的。;比如第一个上升子序列有{18,19,23,24,26},第二个上升子序列有{10,13,16,20},第三个上升子序列有{4,5,6,8},第四个上升子序列有{2,3,5},那么up[0]=26,up[1]=20,up[2]=8,up[3]=5。

down[] 存储当前所有下降子序列的末尾元素,而且要注意down数组它其实是单调递减的;比如第一个下降子序列有{6,4,3},第二个下降子序列有{10,9,6,5},第三个下降子序列有{15,13,12,8},第四个下降子序列为{20,19,16,15,12},那么down[0]=3,down[1]=5,down[2]=8,down[3]=12。

这道题与AcWing1010.拦截导弹的不同之处在于,本题可以使用不同的拦截系统,而那道题只能使用同一种拦截系统。所以本题的大体思路与拦截导弹第二问相同,都是通过贪心来进行构造答案,还要在贪心的外面套一层 dfs用来决定哪种抉择。

贪心思路:

  1. 对于严格单调上升:从前往后扫描每一个数,对于每个数:

    1. 如果现有的全部上升子序列的结尾都大于等于当前数,则创建新的子序列。比如当前数是3,用我们上面的例子,知道up数组存储的是{26,20,8,5},可以知道现有的上升子序列的结尾都是大于当前数3的,也就是说3它不能接到第一、二、三、四个上升子序列中。

    2. 如果现有的全部上升子序列的结尾存在某个元素小于当前数,则不需要新开一个子序列,而是将当前数x接到全部上升子序列中结尾小于它的最大的子序列后面。比如当前数x=23,用我们上面的例子,知道up数组存储的是{26,20,8,5},那么我们应该把当前数x=23接到第二个上升序列后面,即为{10,13,16,20,23}。

  2. 对于严格单调下降:从前往后扫描每一个数,对于每个数:

    1. 如果现有的全部下降子序列的结尾都小于等于当前数,则创建新的子序列。比如当前数x=66,用我们上面的例子,知道down数组存储的是{3,5,8,12},可以知道现有的下降子序列的结尾都是小于当前数66的,也就是说66它不能接到第一、二、三、四个上升子序列中。
    2. 如果现有的全部下降子序列的结尾都大于当前数,则不需要新开一个子序列,而是将当前数x接到全部下降子序列中结尾大于它的最小的子序列后面。比如当前数x=6,用我们上面的例子,知道down数组存储的是{3,5,8,12},结尾大于6的最小的子序列是8,因此我们应该把x=6接到第三个子序列后面,即为{15,13,12,8,6}.

为什么是要接到结尾小于它的最大的子序列后面呢?

比如有上升序列{6,8,15,19,100}和上升序列{3,7,13,16}和上升序列{1,2,6},因此up数组为{100,16,6}.假设来了一个数x=24,如果我们把x=24接到了结尾小于它的最小的子序列后面,即接到了{1,2,6}后面,那么此时序列为{1,2,6,24},那么假设此时又来了一个数x=8,那么这个数x就不能接到上面已有的上升序列,则此时需要再新开一个上升序列来存放这个数x。那我们不就多开了一个新序列嘛,这就不能保证题目要求的最少。如果我们把x=24接到了结尾小于它的最大的子序列后面,即接到了{3,7,13,16}后面,那么此时序列为{3,7,13,16,24},假设此时来了一个数15,那么它还可以接到序列{1,2,6}中,而不用新开一个序列。因此,对于上升子序列来说,小的兼容性更好,因此需要尽量先用大的去消灭。于是优先选用结尾小于它的最大的子序列

为什么要接到结尾大于它的最小的子序列后面呢?

比如有下降序列{10,8,7}和下降序列{15,14,12,10}和下降序列{99,88,77,66,55},因此down数组为{7,10,55},假设来了一个数x=3,如果我们把x=3接到了结尾大于它的最大的子序列后面,即接到了{99,88,77,66,55}后面,那么此时序列为{99,88,77,66,55,3},那么假设此时又来了一个数x=44,那么这个数就不能接到上面已有的下降子序列中,那么就必须新开一个序列来存放这个数x。那我们不就多开了一个新序列嘛,这就不能保证题目要求的最少。如果我们把x=3接到了结尾大于它的最小的子序列后面,即接到了{10,8,7}后面,那么此时序列为{10,8,7,3}。假设又来了一个数x=50,那么它可以接到序列{99,88,77,66,55}中,此时序列为{99,88,77,66,55,50}。因此,对于下降子序列来说,大的兼容性更好,因此需要尽量先用小的去牺牲。于是优先选用结尾大于它的最小的子序列后面。

注意到按照这种贪心思路,up[]数组和down[]数组一定是单调的,因此在遍历时找到第一个满足的序列后就可以直接break了。

最后还需要考虑如何求最小值。因为DFS和BFS不同,第一次搜索到的节点,不一定是步数最短的节点,所以需要进行额外处理。

DFS求最优解一般有两种处理方式:

  • 记录全局最小值,不断更新
  • 迭代加深。一般平均答案深度较低时可以采用这种方式。

代码

迭代加深求DFS最优解

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=60;
int n;
int h[N];
int up[N],down[N];
bool dfs(int depth,int u,int su,int sd)
{
    // 如果上升序列个数 + 下降序列个数 > 总个数是上限,则回溯
    if(su+sd>depth)
    return false;
    //已经枚举完这n个数
    if(u==n)
    return true;
    
     // 枚举放到上升子序列中的情况
    int k=0;
    while(k<su&&up[k]>=h[u])
    {
        k++;
    }
    if(k<su)
    {
        int tmp=up[k];
        up[k]=h[u];
        if(dfs(depth,u+1,su,sd))
        return true;
        up[k]=tmp;
    }
    else
    {
        up[k]=h[u];
        if(dfs(depth,u+1,su+1,sd))
        return true;
    }
    
     // 枚举放到下降子序列中的情况
    k=0;
    while(k<sd&&down[k]<=h[u])
    {
        k++;
    }
    if(k<sd)
    {
        int tmp=down[k];
        down[k]=h[u];
        if(dfs(depth,u+1,su,sd))
        return true;
        down[k]=tmp;
    }
    else
    {
        down[k]=h[u];
        if(dfs(depth,u+1,su,sd+1))
        return true;
    }
    return false;
}
int main()
{
    while(cin>>n,n)
    {
        for(int i=0;i<n;i++)
        scanf("%d",&h[i]);
        int depth=0;
        // 迭代加深搜索
        while(!dfs(depth,0,0,0))
        {
            depth++;
        }
        printf("%d\n",depth);
    }
    return 0;
}
//设置全局最小值ans来求DFS最优解
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=60;
int n;
int h[N];//存储敌方导弹的高度
//up数组存储当前所有上升子序列的末尾元素,up数组里面的元素是单调递减的哦
//down数组存储当前所有下降子序列的末尾元素,down数组里面的元素是单调递增的哦
int up[N],down[N];
int ans;
//u表示枚举到了第几个敌方导弹,su表示上升子序列的个数,sd表示下降子序列的个数
void dfs(int u,int su,int sd)
{
    //如果su+sd已经>=ans,ans已经是最优解了,那么就不可能再找到比ans更优的了,此时回溯就好
    if(su+sd>=ans)
    return;
    //u从第0个数枚举到第n-1个数,一共枚举了n个数,这里不能写成u==n-1,因此这么些的话,当u等于n-1时,是再对第n个数操作
    //但是还没有操作完,如果对第n个数还没有操作完就已经回溯了,那么就没有记录到第n个数,那就是错误的
    //写成u==n,表示已经枚举完了前n个数,现在正在枚举第n+1个数,由于没有第n+1个数,因此此时只需要记录答案,然后回溯即可
    if(u==n)
    {
        ans=min(ans,su+sd);//取最优解ans
        return;
    }
    int k=0;
    //情况1:当该数字插入到上升序列中
    //注意up数组存储的元素是单调递减的,我们要把这个数h[u]接到结尾小于它的最大的子序列后面,循环找到这个位置
    while(k<su&&up[k]>=h[u])
    {
        k++;
    }
    //如果此时k<su,说明是由于up[k]<h[u]而退出的循环,那就说明我们找到了结尾小于它的最大的子序列
    //比如这个子序列为{2,3,14},那么原来up[k]存储就是最后一个数14,假设h[u]=23,那么接到这个子序列后面为{2,3,14,23}
    //所以此时要更新up[k]存储的序列最后一个数23,也就是执行up[k]=h[u]
    if(k<su)
    {
        int tmp=up[k];//保留现场
        up[k]=h[u];//更新
        //这里没有产生新序列,而是接到了某个子序列后面,因此上升子序列个数su不变,继续枚举下一个数u+1
        dfs(u+1,su,sd);
        up[k]=tmp;//恢复现场
    }
    //否则说明是由于k>=su而退出的循环,说明遍历完所有su个上升子序列,都没有找到结尾小于它的最大的子序列
    //那就需要新开一个上升子序列来存储这个数h[u],此时这个序列只有一个数,up[k]存储的就是h[u]这个数
    //因此需要up[k]=h[u]
    else
    {
        up[k]=h[u];//更新
        //此时新开了一个上升子序列,因此上升子序列个数+1,即su+1,继续枚举下一个数u+1
        dfs(u+1,su+1,sd);
    }
    k=0;
    //情况2:当改数字插入到下降序列中
    //注意down数组存储的元素是单调递增的,我们要把这个数h[u]接到结尾大于它的最小的子序列后面,循环找到这个位置
    while(k<sd&&down[k]<=h[u])
    {
        k++;
    }
    //如果此时k<sd,说明是由于down[k]>h[u]而退出的循环,那就说明我们找到了结尾大于它的最小的子序列
    //比如这个序列为{10,8,5},那么down[k]存储的就是5,设h[u]=3,那么此时序列为{10,8,5,3},此时h[u]存储的是3
    //因此需要更新down[k]为存储下降子序列的最后一个数,即down[k]=h[u]
    if(k<sd)
    {
        int tmp=down[k];//保留现场
        down[k]=h[u];//更新
        //这里没有产生新序列,而是接到了某个子序列后面,因此上下降子序列个数sd不变,继续枚举下一个数u+1
        dfs(u+1,su,sd);
        down[k]=tmp;//恢复现场
    }
    //否则说明是由于k>=sd而退出的循环,那就说明遍历完所有的下降子序列都没有找到结尾大于它的最小的子序列
    //则需要新开一个下降子序列来存储h[u],此时这个子序列只有一个数,down[k]存储的就是这个数h[u],因此需要更新down[k]=h[u]
    else
    {
        down[k]=h[u];//更新
        //此时新开了一个下降子序列,因此下降子序列个数+1,即sd+1,继续枚举下一个数u+1
        dfs(u+1,su,sd+1);
    }
}
int main()
{
    while(cin>>n,n)
    {
        //貌似不用清空这三个也能AC
        // memset(up,0,sizeof up);
        // memset(down,0,sizeof down);
        // memset(h,0,sizeof h);
        
        //输入敌方导弹的高度
        for(int i=0;i<n;i++)
        scanf("%d",&h[i]);
        //最坏情况下,每个导弹都是一个序列,成为独立的系统,因此最坏情况下需要n套系统
        ans=n;
        //从第0个导弹,上升子序列为0,下降子序列为0开始枚举爆搜
        dfs(0,0,0);
        printf("%d\n",ans);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值