洛谷 P1020 [NOIP1999 提高组] 导弹拦截问题 以及最大上升子序列的二分优化方式

题目描述

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

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

输入格式

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

输出格式

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

输入输出样例

输入

389 207 155 300 299 170 158 65

输出

6
2

说明/提示

对于前 50% 数据(NOIP 原题数据),满足导弹的个数不超过 10^4 个。该部分数据总分共 100 分。可使用O(n^2) 做法通过。
对于后 50% 的数据,满足导弹的个数不超过 10^5 个。该部分数据总分也为 100 分。请使用 O(nlogn) 做法通过。

对于全部数据,满足导弹的高度为正整数,且不超过 5×10^4 。

题解思路:

对于前 50% 的数据可以采用一般的动态规划方式完成

第一问:设f[i]表示以a[i]结尾的上升子序列的长度最大值,就可以按倒数第二个元素是 a[i-1], a[i-2] ... a[1] 或者 0 来进行划分。

第二问:采用贪心的写法,用一个g[N]数组存储不同子序列末尾的元素值,从前向后扫描a[N]每个数字,那么只会有两种情况

(1)如果现有的所有子序列的结尾数字g[i]都是小于当前数字的(该导弹无法拦截),那就再创建一个新的子序列

(2)如果从前向后遍历发现一个结尾数字大于等于a[i]那么就更新当前子序列结尾的元素,也就是更新g[i],使它变得更小(但是整个g[N]数组一定还是保持单调递减,也就是子序列的末尾元素一定是单调递减的),有点像单调栈的感觉。。

根据Dilworth定理可以得到,在一个序列中,以单调递增的数个序列去覆盖当前序列的数量等于使用单调递减的序列来覆盖的数量。

所以可以得到以下代码:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;
int f[N];
int a[N], g[N], n = 0;

int main()
{
	while (cin >> a[n++]);

	int res = -1;
	n -= 1;
	for (int i = 0; i < n; i++)
	{
		f[i] = 1;                  //最差也应该包括a[i]这个数字,所以长度是1
		for (int j = 0; j < i; j++)
			if (a[j] >= a[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表示在g[]数组里遍历的指针
		while (k < cnt && g[k] < a[i])k++;  // 如果结尾元素 g[k] >= a[i] 跳出
		g[k] = a[i];                        // 无论是否创建新的序列,a[i]都需要放入(无非是放入末尾,或放入头)
		if (k >= cnt)cnt++;                 // 如果遍历完了之后依然没有找到,cnt++,序列个数加1
	}

	cout << cnt << endl;

	return 0;
}

 提交之后belike:

洛谷还是挺诚实的,确实只有100分,不多不少

为什么只对了一半呢?因为时间复杂度太高了。

虽然后半部分的算法是贪心达到了O(nlogn)的时间复杂度,但是前半部分的复杂度达到了O(n^2),综合下来,时间复杂度还是O(n^2),一半的数据就超时了。

那么我们就必须优化一下前面寻找最长子序列的算法了。

对于后 50% 的数据可以利用二分+贪心的算法将时间复杂度降低到 O(nlogn).

AcWing 一位大神做的一个表很形象,我就拿来借用一下

对于序列 3 1 2 1 8 5 6 来寻找最长子序列

a:  3 1 2 1 8 5 6
    ^
f:  3


a:  3 1 2 1 8 5 6
      ^
f:  1


a:  3 1 2 1 8 5 6
        ^
f:  1 2


a:  3 1 2 1 8 5 6
          ^
f:  1 2


a:  3 1 2 1 8 5 6
            ^
f:  1 2 8


a:  3 1 2 1 8 5 6
              ^
f:  1 2 5


a:  3 1 2 1 8 5 6
                ^
f:  1 2 5 6
完成!最后1 2 5 6长度(ans)为4,所以……懂了吧?

原作者:Conan15
链接:https://www.acwing.com/solution/content/71326/
来源:AcWing

简单来说就是遍历a[N],用数组f[N]存储,遍历结束后f[N]中元素的个数就是最长上升子序列的长度,(也就是后面给出的代码中len的大小)。在数组f[N]内进行二分搜索,会出现两种情况:

(1)一旦发现最小的大于a[i]的元素,就将他覆盖掉,此时 len大小不变

(2)将f[N]搜索完后依然没有发现大于a[i]的数字,len++,并存储下此时的a[i].

感觉我说的还没有人家的图清晰

所以就可以优化得到以下代码(ps:题目中说的寻找最长下降子序列)

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 100010;
int n = 0;
int a[N], g[N], f[N];

int main()
{
	while (cin >> a[n])n++;

	int len = 0;
	for (int i = 0; i < n; i++)
	{
		int pos = upper_bound(f, f + len, a[i],greater<int>()) - f;
		if (pos == len) f[len++] = a[i];
		else f[pos] = a[i];
	}
	cout << len << 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;
}

简单解释一下 upper_bound<> 使用了greater<int>重载之后,相当于在f数组里面找 <a[i] 的最大值,pos是两个指针相减,得到的是元素的位置。

如果没有找到,则返回末尾元素,也就是pos==len的时候,长度len++,序列长度变长。

然后就过了

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值