一、 概述
该题也是一个典型的最长上升子序列的问题。
- 分析
导弹拦截高度要么一直上升要么一直下降。
有的导弹可以选择上升,有的可以选择下降,不是单纯地问所存在的序列可以划分为多少组上升子序列的问题【狄尔沃斯定理】,所以不能用之前的方法解。
-
怎么做
当找不到办法时,考虑使用枚举法(暴力搜索\(dfs\)) -
如何做
从问题的解出发,最终问题的答案是有许多单调上升子序列和许多单调下降子序列,那么实际就是对于每个数,来思考将该数放到上升序列中还是下降序列中。 -
题解
-
注意顺序性
-
依次枚举每个数
(1) 先枚举将该数作为单调上升的序列,还是单调下降的序列中
(2) 如果该数被放到了单调上升的序列中,则枚举将该数放到哪个单调上升的序列后面
(3) 如果该数被放到了单调下降的序列中,则枚举将该数放到哪个单调下降的序列后面。
(4) 或者该数成为一个单独的单调序列
\(up[]\) 存储当前所有上升子序列的末尾元素
\(down[]\) 存储当前所有下降子序列的末尾元素
所以这里实际上用到了上一题导弹防御的结果来做,对于多个单调上升(下降)子序列只需要对末尾元素进行考虑即可。这一点真厉害!!!然后这里又可以进行优化。
-
使用贪心的思想
\(x\) 尽可能覆盖一个末尾元素大的序列
因此是一个很强的优化,所以每一步实际上只有两种选择,要么上升,要么下降。
\(up\)本身是单调的,所以一方面找到即停止,另一方面可以直接考虑用二分
-
如何进行搜素
-
使用\(BFS\), \(BFS\)是宽度优先,需要进行存储(空间太多,不太好剪枝),而且代码写起来比较麻烦
-
使用\(dfs\), 一种想法是使用全局变量,第二种方式是迭代加深
假设现在要把一个数放入一个上升序列,那么一定是所有能放入的上升序列中,最后一个元素最大的那一个。
其实想想也是,既然每个数字都要放到一个序列中,对于上升序列,肯定是目前越小越有用,既然能放入大的里面,何必浪费一个小的呢
注意到其实\(up[i]\)按这种策略已经是排好序的了,所以只用找最先碰到的一个就行了
二、dfs实现代码
\(dfs\) \(560ms\) 时间\(O(n2^n)\) 空间\(O(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
int n;
int a[N];
int up[N], down[N];
int res;
/*总结:
1、面向答案编程,问什么问题,就设计dfs的参数是什么。比如本题关注几套拦截系统,su就是上升拦截系统数量,sd就是下降拦截系统数量
,两者的和就是答案。
2、数字适合做为dfs参数进行传送,数组之类的适合静态数组+回溯法处理,一般不建议采用vector,一来是慢,二来是写法麻烦。
* 功能:暴搜所有可能,配合剪枝,找出最少的拦截系统数量
* @param step 第几个导弹
* @param su 上升拦截系统的数量,配合up数组使用,表示 len(up[])
* @param sd 下降拦截系统的数量,配合down数组使用,表示 len(down[])
*/
void dfs(int step, int su, int sd) {
if (su + sd >= res) return; //中途发现已经大于等于res的情况,就返回,剪枝
if (step == n + 1) { //收集答案
//本来是:res = min(res, su + sd);
//简写:
res = su + sd;//原因是上面那句话把>=res的都返回了,到这里肯定是小于res的,所以上面的min就没有意义
return;
}
// 情况1:将当前数放到上升子序列中
int k;
for (k = 0; k < su; k++) if (up[k] < a[step])break;//找到合适位置
//回溯法
int t = up[k]; //拷贝副本
up[k] = a[step]; //是多开一套,还是加在某套的后面,两种情况都需要记录下数据
if (k < su) dfs(step + 1, su, sd);//只更新数据即可,长度不用长大
else dfs(step + 1, su + 1, sd);//多开一套,up数组长度长大1
up[k] = t; //恢复现场
// 情况2:将当前数放到下降子序列中
for (k = 0; k < sd; k++) if (down[k] > a[step])break;//找到合适位置
//回溯法
t = down[k];
down[k] = a[step];
if (k < sd) dfs(step + 1, su, sd);
else dfs(step + 1, su, sd + 1);
//恢复现场
down[k] = t;
}
int main() {
//多套数据,输入n=0时停止
while (cin >> n, n) {
for (int i = 1; i <= n; i++) cin >> a[i];
//防御系统的最少数量
res = n;
//开始深搜,更新res的值
dfs(1, 0, 0);
printf("%d\n", res);
}
return 0;
}
三、迭代加深
迭代加深+普通 \(541ms\) 时间\(O(n2^n)\) 空间\(O(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
int n;
int a[N];
int up[N], down[N];
/**
* 功能:迭代加深模板下的业务代码
* @param depth 限制深度
* @param step 第几个导弹
* @param su 拦截上升导弹系统个数
* @param sd 拦截下降导弹系统个数
* @return 是否能在规定深度的情况下完成目标
*/
bool dfs(int depth, int step, int su, int sd) {
// 深度从0开始,迭代加深套路
if (su + sd > depth) return false;
if (step == n + 1) return true;//在规定防御系统深度的情况下,可以满足要求,就是找到了答案
// 枚举上升子序列 up是单调上升的,想要在up中找到第一个比a[step]小的数字
int k;
for (k = 0; k < su; k++) if (up[k] < a[step]) break;
int t = up[k];
up[k] = a[step];
//找到合理解,就直接返回;没有找到合理解,那么还需要继续计算,不能返回
//换句话说就是:中间只允许返回true,不到最后,不允许返回false
if (k < su && dfs(depth, step + 1, su, sd)) return true;
else if (dfs(depth, step + 1, su + 1, sd)) return true;
up[k] = t;
// 枚举下降子序列
for (k = 0; k < sd; k++) if (down[k] > a[step])break;
t = down[k];
down[k] = a[step];
if (k < sd && dfs(depth, step + 1, su, sd)) return true;
else if (dfs(depth, step + 1, su, sd + 1)) return true;
down[k] = t;
//实在没招了,返回false
return false;
}
int main() {
while (cin >> n, n) {
//输入
for (int i = 1; i <= n; i++) cin >> a[i];
//迭代加深的套路
//本题中深度可以理解为最大允许的防御导弹系统数量
int depth = 0;
while (!dfs(depth, 1, 0, 0)) depth++;
//迭代加深的深度就是答案
printf("%d\n", depth);
}
return 0;
}