2025.4.10刷题总结1 P1020 [NOIP 1999 提高组] 导弹拦截(dp)

# P1020 [NOIP 1999 提高组] 导弹拦截

## 题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

   
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

## 输入格式

一行,若干个整数,中间由空格隔开。

## 输出格式

两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

## 输入输出样例 #1

### 输入 #1

```
389 207 155 300 299 170 158 65
```

### 输出 #1

```
6
2
```




刚开始看题,想到了单调子序列的问题,但还是写不出状态转移方程;看了看题解之后开始思考写状态转移方程的技巧

  1. 无论如何,一定要想清楚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 个导弹,就无法体现出 “以后每一发炮弹都不能高于前一发的高度” 这个限制条件。

     

    比如,当考虑第 i 个导弹时,若它的高度高于前面已经拦截的某一个导弹,那么它就不能被拦截,不能单纯地只考虑选或不选。所以,这里定义 dp[i] 为以第 i 个元素结尾的最长不上升子序列的长度,这样能更好地体现出序列的连续性和不上升的特性。      其实经过总结后发现有两种本质一样的不同形式的方案。一是 :对于求最长不上升子序列的长度,我们可以使用动态规划的方法。定义一个数组 dp,其中 dp[i] 表示以第 i 个元素结尾(把第i个导弹确定加入一个序列,思维简答)的最长不上升子序列的长度。对于每个元素 i,我们遍历其前面的所有元素 j0 <= 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 个导弹,有两种情况:

    (1)不拦截第 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;
    }    

  2. 接下来写一下动态规划的思路

类似动态规划问题的思考方法

1. 明确问题的状态

  • 寻找问题的子问题:把原问题分解成一系列规模更小的子问题。像在导弹拦截问题中,原问题是求整个导弹序列的最长不上升子序列长度,子问题就是求以每个导弹为结尾的最长不上升子序列长度。
  • 定义状态变量:依据子问题来定义状态变量。例如,在最长不上升子序列问题中,定义 dp[i] 表示以第 i 个导弹高度结尾的最长不上升子序列的长度;在背包问题中,定义 dp[i][j] 表示考虑前 i 个物品,背包容量为 j 时能获得的最大价值。

2. 找出状态转移方程

  • 分析状态之间的关系:思考子问题之间是如何相互关联的,也就是一个状态是如何由其他状态推导出来的。在导弹拦截问题中,对于 dp[i] ,要遍历前面所有的 j0 <= 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​。考虑二分。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值