# P1020 [NOIP 1999 提高组] 导弹拦截
## 题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
## 输入格式
一行,若干个整数,中间由空格隔开。
## 输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
## 输入输出样例 #1
### 输入 #1
```
389 207 155 300 299 170 158 65
```
### 输出 #1
```
6
2
```
刚开始看题,想到了单调子序列的问题,但还是写不出状态转移方程;看了看题解之后开始思考写状态转移方程的技巧
- 无论如何,一定要想清楚dp[i]是什么含义,刚开始想的是dp[i]是考虑到第i个点(倒着考虑,并且还有一个i后最长段中的最高点的记录与之对应),如果这时候h[i]大于max,则dp[i]=dp[i+1]+1且max长的高点变为h[i],但问题是顺序太混乱且考虑的因素太繁杂。于是冥思苦想后发现在背包问题里,
dp[i]
(一般是二维的dp[i][j]
,i
表示考虑前i
个物品,j
表示背包容量)可以从考虑第i
个物品选或者不选来进行状态转移。这是因为背包问题里,物品之间相互独立,选或者不选第i
个物品不会影响前面物品的选择情况。
但在导弹拦截问题求最长不上升子序列时,对于第
i
个导弹,它能否被拦截不仅取决于自身,还和前面已经拦截的导弹高度有关。如果简单考虑选或者不选第i
个导弹,就无法体现出 “以后每一发炮弹都不能高于前一发的高度” 这个限制条件。比如,当考虑第
(1)不拦截第i
个导弹时,若它的高度高于前面已经拦截的某一个导弹,那么它就不能被拦截,不能单纯地只考虑选或不选。所以,这里定义dp[i]
为以第i
个元素结尾的最长不上升子序列的长度,这样能更好地体现出序列的连续性和不上升的特性。 其实经过总结后发现有两种本质一样的不同形式的方案。一是 :对于求最长不上升子序列的长度,我们可以使用动态规划的方法。定义一个数组dp
,其中dp[i]
表示以第i
个元素结尾(把第i个导弹确定加入一个序列,思维简答)的最长不上升子序列的长度。对于每个元素i
,我们遍历其前面的所有元素j
(0 <= j < i
),如果height[j] >= height[i]
,说明可以将第i
个元素加入到以第j
个元素结尾的不上升子序列中,此时更新dp[i]
的值为max(dp[i], dp[j] + 1)
。第二种方案:可以尝试用dp[i][j]
来表示状态,其中i
表示第几个导弹,j
表示当前拦截系统所能拦截的最低高度,不过这种状态定义在本题情境下会让问题变得复杂,下面详细分析。状态定义:dp[i][j]
表示考虑前i
个导弹,且当前拦截系统所能拦截的最低高度为j
时,最多能拦截的导弹数量。状态转移:对于第i
个导弹,有两种情况:i
个导弹:此时dp[i][j] = dp[i - 1][j]
,即拦截情况和只考虑前i - 1
个导弹且最低高度为j
时一样。 (2)拦截第i
个导弹:前提是第i
个导弹的高度h[i]
不高于当前最低高度j
,此时dp[i][h[i]] = max(dp[i][h[i]], dp[i - 1][j] + 1)
,也就是更新考虑前i
个导弹且最低高度变为h[i]
时的最大拦截数量。#include <iostream> #include <vector> #include <algorithm> using namespace std; const int MAX_HEIGHT = 1000; int main() { vector<int> heights; int height; // 读取输入的导弹高度 while (cin >> height) { heights.push_back(height); } int n = heights.size(); // 初始化 dp 数组 vector<vector<int>> dp(n + 1, vector<int>(MAX_HEIGHT + 1, 0)); // 状态转移 for (int i = 1; i <= n; ++i) { int h = heights[i - 1]; for (int j = 0; j <= MAX_HEIGHT; ++j) { // 不拦截第 i 个导弹 dp[i][j] = dp[i - 1][j]; if (h <= j) { // 拦截第 i 个导弹 dp[i][h] = max(dp[i][h], dp[i - 1][j] + 1); } } } // 找出最大拦截数量 int ans = 0; for (int j = 0; j <= MAX_HEIGHT; ++j) { ans = max(ans, dp[n][j]); } // 计算拦截所有导弹最少需要的系统数量(最长上升子序列) vector<int> dp2(n, 1); for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { if (heights[j] < heights[i]) { dp2[i] = max(dp2[i], dp2[j] + 1); } } } int min_systems = 0; for (int len : dp2) { min_systems = max(min_systems, len); } // 输出结果 cout << ans << endl; cout << min_systems << endl; return 0; }
- 接下来写一下动态规划的思路
类似动态规划问题的思考方法
1. 明确问题的状态
- 寻找问题的子问题:把原问题分解成一系列规模更小的子问题。像在导弹拦截问题中,原问题是求整个导弹序列的最长不上升子序列长度,子问题就是求以每个导弹为结尾的最长不上升子序列长度。
- 定义状态变量:依据子问题来定义状态变量。例如,在最长不上升子序列问题中,定义
dp[i]
表示以第i
个导弹高度结尾的最长不上升子序列的长度;在背包问题中,定义dp[i][j]
表示考虑前i
个物品,背包容量为j
时能获得的最大价值。
2. 找出状态转移方程
- 分析状态之间的关系:思考子问题之间是如何相互关联的,也就是一个状态是如何由其他状态推导出来的。在导弹拦截问题中,对于
dp[i]
,要遍历前面所有的j
(0 <= j < i
),如果height[j] >= height[i]
,说明可以把第i
个导弹加入到以第j
个导弹结尾的不上升子序列中,此时dp[i] = max(dp[i], dp[j] + 1)
。 - 确定边界条件:明确状态转移的起始点。例如,在最长不上升子序列问题中,每个元素自身都可以构成一个长度为 1 的不上升子序列,所以
dp[i]
初始值都为 1。
3. 计算最终结果
- 根据状态转移方程计算:按照状态转移方程,从边界条件开始逐步计算出所有状态的值。
- 提取最终答案:根据问题的要求,从计算得到的状态中提取出最终的答案。比如在导弹拦截问题中,最终答案是
dp
数组中的最大值。
这是我的比较模糊的总结,以后还需要改进
再加一点吧
1.先根据最优子结构和子问题重叠判断出用动态规划
2.寻找子问题,有多种可能,多种思路
3.定义状态变量dp
4.找出状态转移方程
5.其余细节操作(边界条件,递归还是递推.....)
补充一下第二问:
计算拦截所有导弹最少需要配备多少套系统,根据 Dilworth 定理,这等价于求给定高度序列中的最长上升子序列(Longest Increasing Subsequence,LIS)的长度。
优化的话就用贪心
一套系统最多拦截导弹数:可以维护一个数组 tail
,用于记录当前最长不上升子序列的末尾元素(index就是序列长度)。遍历导弹高度序列,对于每个导弹高度 h
,如果 h
不大于 tail
数组的最后一个元素,就将 h
添加到 tail
数组末尾;否则,通过二分查找在 tail
数组中找到第一个小于 h
的元素,并用 h
替换它。最终 tail
数组的长度就是最长不上升子序列的长度。
一下引用洛谷的一个题解的证明
下面考虑优化。
记 fi 表示「对于所有长度为 i 的单调不升子序列,它的最后一项的大小」的最大值。特别地,若不存在则 fi=0。下面证明:
随 i 增大,fi 单调不增。即 fi≥fi+1。
考虑使用反证法。假设存在 u<v,满足 fu<fv。考虑长度为 v 的单调不升子序列,根据定义它以 fv 结尾。显然我们可以从该序列中挑选出一个长度为 u 的单调不升子序列,它的结尾同样是 fv。那么由于 fv>fu,与 fu 最大相矛盾,得出矛盾。
因此 fi 应该是单调不增的。
现在考虑以 i 结尾的单调不升子序列的长度的最大值 dpi。由于我们需要计算所有满足 h(j)>h(i) 的 j 中,dpj 的最大值,不妨考虑这个 dpj 的值是啥。设 dpj=x,那么如果 h(i)>fx,由于 fx≥h(j),就有 h(i)>h(j),矛盾,因此总有 h(i)≤fx。
根据刚刚得出的结论,fi 单调不增,因此我们要找到尽可能大的 x 满足 h(i)≤fx。考虑二分。