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

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

原题链接:P1020 [NOIP 1999 提高组] 导弹拦截 - 洛谷

题目描述

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

输入格式

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

输出格式

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

输入输出样例 #1

输入 #1

389 207 155 300 299 170 158 65

输出 #1

6
2

说明/提示

对于前 50 % 50\% 50% 数据(NOIP 原题数据),满足导弹的个数不超过 1 0 4 10^4 104 个。该部分数据总分共 100 100 100 分。可使用 O ( n 2 ) \mathcal O(n^2) O(n2) 做法通过。
对于后 50 % 50\% 50% 的数据,满足导弹的个数不超过 1 0 5 10^5 105 个。该部分数据总分也为 100 100 100 分。请使用 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 做法通过。
对于全部数据,满足导弹的高度为正整数,且不超过 5 × 1 0 4 5\times 10^4 5×104
NOIP1999 提高组 第一题

算法

动态规划+贪心思想

算法思路

题目要求两个输出,

第一个数字表示这套系统最多能拦截多少导弹,
也就是求给出的数列的最长非上升子序列(后一个导弹的高度不高于前一个导弹的高度)的长度,是一个简单的LIS问题,直接用动态规划算法解决。

第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统,
也就是求出至少用多少个非上升子序列可以完全覆盖所有的数列元素,
从前往后遍历数列的每一个数,数列的第一个数要用一个导弹拦截系统进行拦截,从第二个数开始的每一个数都有两种选择,第一种选择是接到现有的某个导弹拦截系统后面,第二种选择是再开一个新的导弹拦截系统。那如何做出选择呢?无论哪一种选择,当前数都会是某个导弹拦截系统的最后一个数,要使导弹拦截系统的数量尽可能的小,贪心地想,在选择之后,要让除当前数所在导弹导弹拦截系统外的所有系统的最后一个数尽可能的大,也就是要找到大于等于当前数的最小导弹系统结尾数,把当前数接在这个导弹系统的后面,这样在后面的选择中,这些系统能容纳高度更高的导弹,从而使导弹拦截系统的数量尽可能的小。

贪心算法正确性的证明:(其实记住就行,做一道记一道,这道题的贪心思路是很经典的)
在贪心算法中要证明A=B的方法:证明A>=B,同时A<=B,则A=B。
A表示贪心算法的结果数,B表示最优解的答案数
①由于答案求的就是最小值,所以B<=A。
②证明A<=B,用调整法证明,假设贪心算法的方案与最优解的方案不同,找到第一个不同的位置,
在这里插入图片描述

假设a是大于等于x的最小的结尾数,贪心思路把x放在a的后面,而最优解把x放在了b的后面,由于a是大于等于x的最小的数,所以b>=a一定成立,然后继续完成后面的数的选择,
在这里插入图片描述

由于a和b都是大于等于x的,所以可以将最优解的红色方框与贪心法的蓝色方框进行交换,且交换过程中并没有增加导弹拦截系统的个数,按照这个方法,通过若干次调整可以将最优解的方案调整为贪心算法的方案,且
调整过程中导弹拦截系统的个数没有增加,所以贪心法的方案结果数小于等于最优解的方案结果数,即A<=B。

所以,
如果当前所有导弹拦截系统的结尾数都比当前要进行选择的数要小,就重新开一个导弹拦截系统;
否则,找到结尾数大于等于当前要进行选择的数且结尾数最小的导弹拦截系统,让当前数接到这个系统的后面;

在代码实现过程中,我们用一个g[]数组来存储所有导弹拦截系统的最后一枚导弹的高度,
选择一:重新开一个导弹拦截系统,要在g[]数组后面加上当前数,由于选择一的前提就是当前数大于所有的结尾数,所以插入的数一定是大于之前g[]数组的结尾数的。
选择二:在某个导弹拦截系统后面插入当前数,
在这里插入图片描述

a是g数组中最小的大于等于x的数,所以选择二将当前数插入a所在导弹拦截系统之后,相当于把g[]数组中的a换成了x,很容易发现如果原来的g[]数组是单调上升的,在进行选择二的操作之后,g[]数组仍然是单调上升的。
由此,我们可以发现g[]数组的个数最后就是整个序列的最长上升子序列的长度。
在本题中,g[]数组的元素个数代表着有多少个导弹拦截系统,也就是最少用多少个非上升子序列来覆盖给出的序列。
所以,我们得出拓展结论:
一个序列的最长上升子序列元素个数等于最少用多少个非上升子序列覆盖这个序列的答案数,
这其实就是Dilworth 定理,
Dilworth 定理:在任何有限的偏序集中,其最小链划分的大小等于最长反链的大小。
本题要求的是最少用多少个非上升子序列(元素满足>=关系)能够覆盖给出的序列,答案就等于最长上升子序列(元素满足<关系)的长度。

C++ 代码1:动态规划+贪心思想

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N];
int f[N], g[N];
int main()
{
	while (cin >> q[n])
	{
		n++;
	}
	//求最长非上升子序列的长度
	int res = 0;
	for (int i = 0; i < n; i++)
	{
		f[i] = 1;
		for (int j = 0; j < i; j++)
		{
			if (q[j] >= q[i])
			{
				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;//k表示遍历到哪一个序列
		if (k < cnt && g[k] < q[i])//如果还没有遍历完所有创建的导弹系统序列且当前导弹系统的最后一个数小于当前数
		{
			k++;
		}
		g[k] = q[i];
		//如果是选择一,相当于在数组末尾插入一个数,只不过在后面要对cnt进行++
		//如果是选择二,就是用x替换g[]数组中的a
		if (k >= cnt)//如果是选择一,cnt++
		{
			cnt++;
		}
	}
	cout << cnt << endl;
	return 0;
}

时间复杂度

O(N^2)

C++ 代码1:动态规划+贪心思想+二分查找

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N];
int f[N], g[N];
int binary_search1(int l, int r, int x)//找到第一个小于x的数
{
	while (l < r)
	{
		int mid = l + r >> 1;
		if (f[mid] >= x)
		{
			l = mid + 1;
		}
		else
		{
			r = mid;
		}
	}
	return l;
}
int binary_search2(int l, int r, int x)//找到第一个大于等于x的数
{
	while (l < r)
	{
		int mid = l + r >> 1;
		if (g[mid] < x)
		{
			l = mid + 1;
		}
		else
		{
			r = mid;
		}
	}
	return l;
}
int main()
{
	while (cin >> q[n])
	{
		n++;
	}
	//求最长非上升子序列的长度
	f[0] = q[0];
	int len = 1;
	for (int i = 1; i < n; i++)
	{
		if (q[i] <= f[len - 1])//如果当前导弹高度小于等于导弹拦截系统当前最后一枚导弹的高度,就直接接到导弹拦截系统的后面
		{
			f[len] = q[i];
			len++;
		}
		else//否则的话,找到f[]数组中第一个小于(而不是小于等于)当前数的数,用当前数替换这个数
		{
			int x = binary_search1(0, len - 1, q[i]);
			f[x] = q[i];
		}
	}
	cout << len << endl;
	//求最少用多少个非上升子序列覆盖给出的序列,相当于找到序列的最长上升子序列的长度
	g[0] = q[0];
	int cnt = 1;
	for (int i = 1; i < n; i++)
	{
		if (q[i] > g[cnt - 1])//如果当前数大于g[]数组最后一个数的话,直接加入g[]数组,相当于又开了一个导弹拦截系统
		{
			g[cnt] = q[i];
			cnt++;
		}
		else//否则的话,在g[]数组中找到第一个大于等于当前数的数,用当前数替换这个数
		{
			int x = binary_search2(0, cnt - 1, q[i]);
			g[x] = q[i];
		}
	}
	cout << cnt << endl;
	return 0;
}

注意,代码中的两部分是有区别的,
求最长非上升子序列长度的时候,二分查找的是f[]数组中第一个小于q[i]的数;
求最长上升子序列长度的时候,二分查找的是g[]数组中第一个大于等于q[i]的数;
只有当求的是最长下降子序列长度的时候,二分查找的才是f[]数组中第一个小于等于q[i]的数。

时间复杂度

O(NlogN)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值