文章目录
0. 前言
强相关:
[线性dp] 最长上升子序列(模板题+最长上升子序列模型) 贪心解法
本题进阶版:
[线性dp] 导弹防御系统(最长上升子序列模型+贪心+dfs)
1. LIS +贪心
重点: 线性 dp
、LIS 问题、贪心
思路:
- 问题就是最少用多少个下降子序列来覆盖掉整个子序列
- 首先第一问就是寻找最长的下降子序列。 可以直接转化为
LIS
问题 - 第二问为贪心问题,首先简单说明下贪心直觉和证明方法
- 假设现在存在很多个子序列,那么当前遍历到的这个导弹不管接到那个子序列的后面,目前来讲,它都是做这些子序列的结尾元素。那么,能接上去的前提是这些子序列的末尾要大于当前数。根据常理直觉来讲,应该是要接到所有大于当前数的最小末尾子序列的后面,这样高度不会出现断崖式下降。当然,所有的子序列末尾都小于当前高度,就得自己成为一个新的子序列头部
- 证明该贪心思路:
- 假设
A
表示贪心法得到的序列个数,B
表示最优解 - 显然,
B<=A
- 通过反证法证明
A<=B
:找到贪心法和最优解所有子序列中第一个数字相同但是位置不同的数,那么由于贪心法一定是所有子序列末尾最小的原则,将该数加到了该子序列的后面,所有贪心法所处的末尾数字一定是小于等于最优解的末尾数字的(在此末尾数字都是添加之前),那么我们可以将这两个子序列后面的所有数字进行一个交换,交换后仍为一个合法方案,最终通过这样的调整,一定能将贪心法调整为最优解形式。并且,这样的调整并没有增加子序列的个数,且最多只需要调整n
次就可以达到一致,故A <= B
- 贪心思路的证明,大多使用调整法或者反证法来进行证明
- 假设
- 基于这个思想,第一问就写个
LIS
就行了。第二问实现方式为:- 开一个数组
g
来存储当前所有已经开好的子序列结尾的数。枚举当前数x
时,就每次在g
数组中寻找大于等于它的最小的数,将其更新就行了。如果这个数比所有g
中所有数都大,则说明这个导弹高度很高,需要自己成为一个导弹系统,就把它放到g
数组末尾即可 - 按照如上操作,
g
序列一定是单调上升的。这里涉及到 4 个数,更新位置的那个数是b
,它的前一个是a
,后一个是c
,显然,一开始满足,a<b<=c
,三者与x
的关系是:a<x<=b<=c
,那么我们将b
更新为x
,也是满足a<x<=c
的,所以序列可以稳定单调上升 - 其实能够发现,这里的贪心思路及代码实现和 [线性dp] 最长上升子序列(模板题+最长上升子序列模型) 中
LIS
问题的贪心+二分的 O ( n l o g n ) O(nlogn) O(nlogn),解法都完全一样。所以这两个问题是等价的,故求最多用多少个上升子序列将序列覆盖和最少用多少个下降子序列将整个序列覆盖的答案是一样的,故这两个问题是对偶问题。所以本题可以将数据范围增加到10w
,必须使用 n l o g n nlogn nlogn 的算法才能AC
- 开一个数组
[线性dp] 最长上升子序列(模板题+最长上升子序列模型) 和 LIS
贪心+二分的思路强相关。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int n;
int a[N], g[N];
int f[N];
int main() {
// stringstream 的方式数据读取
/*
* string line;
* getline(cin, line);
* stringstream ssin(line);
* while (ssin >> h[n]) n ++ ;
*/
while (cin >> a[n]) ++n; // 得到输入数字的个数
int res = 0;
for (int i = 0; i < n; ++i) {
f[i] = 1;
for (int j = 0; j < i; ++j)
if (a[i] <= a[j]) // 在此求最长下降子序列
f[i] = max(f[i], f[j] + 1);
res = max(res, f[i]);
}
cout << res << endl;
int cnt = 0; // 记录所有子序列末尾数字
for (int i = 0; i < n; ++i) {
int k = 0;
while (k < cnt && g[k] < a[i]) ++k; // 找到第一个比自己
g[k] = a[i];
if (k >= cnt) cnt ++;
}
cout << cnt << endl;
return 0;
}