算法
(DFS,迭代加深,剪枝,贪心) $O(n2^n)$
为了能遍历所有情况,我们首先考虑搜索顺序是什么。
搜索顺序分为两个阶段:
- 从前往后枚举每颗导弹属于某个上升子序列,还是下降子序列;
- 如果属于上升子序列,则枚举属于哪个上升子序列(包括新开一个上升子序列);如果属于下降子序列,可以类似处理。
因此可以仿照AcWing 896. 最长上升子序列 II,分别记录当前每个上升子序列的末尾数up[],和下降子序列的末尾数down[]。这样在枚举时可以快速判断当前数是否可以接在某个序列的后面。
注意这里的记录方式和上一题稍有不同:
- 这里是记录每个子序列末尾的数;
- 上一题是记录每种长度的子序列的末尾最小值。
此时搜索空间仍然很大,因此该如何剪枝呢?
对于第二阶段的枚举,我们可以仿照上一题的贪心方式,对于上升子序列而言,我们将当前数接在最大的数后面,一定不会比接在其他数列后面更差。
这是因为处理完当前数后,一定出现一个以当前数结尾的子序列,这是固定不变的,那么此时其他子序列的末尾数越小越好。
注意到按照这种贪心思路,up[]数组和down[]数组一定是单调的,因此在遍历时找到第一个满足的序列后就可以直接break了。
最后还需要考虑如何求最小值。因为DFS和BFS不同,第一次搜索到的节点,不一定是步数最短的节点,所以需要进行额外处理。
一般有两种处理方式:
时间复杂度
每个数在第一搜索阶段有两种选择,在第二搜索阶段只有一种选择,但遍历up[]和down[]数组需要 $O(n)$ 的计算量,因此总时间复杂度是 $O(n2^n)$。
C++ 代码
#include <cstring>
#include <iostream>
#include <algorithm>
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;
if (u == n) return true;
// 枚举放到上升子序列中的情况
bool flag = false;
for (int i = 1; i <= su; i ++ )
if (up[i] < h[u])
{
int t = up[i];
up[i] = h[u];
if (dfs(depth, u + 1, su, sd)) return true;
up[i] = t;
flag = true;
break; // 注意由上述证明的贪心原理,只要找到第一个可以放的序列,就可以结束循环了
}
if (!flag) // 如果不能放到任意一个序列后面,则单开一个新的序列
{
up[su + 1] = h[u];
if (dfs(depth, u + 1, su + 1, sd)) return true;
}
// 枚举放到下降子序列中的情况
flag = false;
for (int i = 1; i <= sd; i ++ )
if (down[i] > h[u])
{
int t = down[i];
down[i] = h[u];
if (dfs(depth, u + 1, su, sd)) return true;
down[i] = t;
flag = true;
break; // 注意由上述证明的贪心原理,只要找到第一个可以放的序列,就可以结束循环了
}
if (!flag) // 如果不能放到任意一个序列后面,则单开一个新的序列
{
down[sd + 1] = 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 ++ ) cin >> h[i];
int depth = 0;
while (!dfs(depth, 0, 0, 0)) depth ++ ; // 迭代加深搜索
cout << depth << endl;
}
return 0;
}
最后 那个depth 的数目为什么就代表 导弹的个数呢?求大佬讲解一下谢谢!!
前面枚举过的所有$depth$不能使题目条件都成立的都不能作为答案,而第一个枚举到能作为答案的$depth$满足题目的要求(dfs返回1了),故跳出循环的时候depth的值就是满足题意的最小值答案
$depth-1$不满足题意,但是$depth$满足,而$depth$一定是一个整数,所以可以得到这个结论
你可以搜一下,什么叫迭代加深,迭代加深就用到了depth
在做拦截导弹的时候看到了两种做法,其中有一种做法的递归是要就是像这里的 if (dfs(depth, u + 1, su, sd)) return true; 另一种直接dfs(u+1,su,sd); 经常看到这两种写法,自己做的时候就在想用哪种。想请教一下这两种都适用于什么情况还是或者说其实都一样。
找到一个解的时候就可以返回了,不返回的话会多搜很多冗余状态,比较慢。
所以说是因为迭代加深第一次找到解就是答案,所以就可以直接返回了,不用继续搜下去。而另一种方法因为是暴力搜索,第一次找到的不一定是答案,所以要继续搜吧。谢谢yls
对滴,如果不用迭代加深的话,就不能直接返回了。
懂啦,谢谢。
这里的。if(dfs…)如果返回true的话,它底下的flag=true会执行吗?
不会了,说明已经找到一组解了,没有必要再处理其他内容了。
这不能保证up是单调增的吧?如果up[i+1]也是小于h[u]的呢?
我认为这个循环应该倒着搜吧?不过我这样作在提交时,第一个样例报错了。求y总讲解
up数组是单调减的。
up和down都是单调的,是不是可以用二分呢?
可以的,应该会快一些
二分可以过
https://www.acwing.com/activity/content/code/content/128088/
棒
我用upper_bound二分居然TLE
这道题目的时限卡得很松,可能是哪里写错了。
https://www.acwing.com/problem/content/discussion/content/393/
两个upper_bound都跑就挂,只跑一个就过。不知道为什么超时。很坑啊。
upper_bound也像deque一样很坑吗?
可能有两方面原因,一是大部分情况下数组都很短,二分收益不高;二是upper_bound本身常数较大。
谢谢。这种情况真心坑啊。本想偷个懒直接拿来用,反倒浪费很多时间调试。
平时多踩踩坑不是坏事,就当给考试排雷了。
这里属于你acwing的测评机拉跨了.
详见: https://www.acwing.com/problem/content/discussion/content/393/.
看其中我的评论.
这里n=55, $2^{55}$这个计算量为什么不会超时呢?不是很理解这个优化的时间复杂度分析部分.
首先答案很小,也就是需要用到的单调序列的个数很少,因此
dfs()
中的depth
这个参数就很小,那么if (su + sd > depth) return false;
这个剪枝就会剪掉很多分支了。嗯了解!
好奇怪,dfs的时候先枚举down就会TLE
这题可不可以up数组单调递增 if语句直接up[i]>h[u]的话就更改up[i- 1]
下面这段代码应该恢复现场吧?
if (!flag) // 如果不能放到任意一个序列后面,则单开一个新的序列
{
up[su + 1] = h[u];
if (dfs(depth, u + 1, su + 1, sd)) return true;
up[su + 1] = 0;//这里得恢复现场????
}
这种情况等价于操作本身就保证了恢复现场,当从其他分支进入dfs(depth,u,su,sd)这个函数时,此时up数组的长度是su而非su+1,就相当于把up[su+1]位置上的数删除了,再次进入你这段代码时up[su + 1] = h[u];会把之前的数据覆盖掉
y总,flag = true代表u未被放入序列吧?
那为什么回溯时要把它标记为false呢,回溯不是代表放入序列这步操作取消了吗?
这里对flag的操作不是回溯,仅仅是判断一下当前的数能否接在某个序列的后面,如果不能接在任何一个序列的后面,就新开一个序列。
谢谢y总。
客气啦
大佬,您能给代码加一下注释吗,感觉看不太懂
好滴,视频题解来啦,里面有详细讲解hh
代码注释已添加~