P1020 [NOIP1999 普及组] 导弹拦截(100+200+详细证明)

那么题意:

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

输入导弹依次飞来的高度(雷达给出的高度数据是\le 50000≤50000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

思路:

第一问不难想到是个很明显的最长不上升子序列,就是最长下降子序列,那么把写个是个经典模板题没跑了,简单来说用y总的dp三部曲就是:

状态表示:f[i]表示从第一个数字开始算,以w[i]结尾的最大的上升/下降序列。(以w[i]结尾的所有上升/下降序列中属性为最大值的那一个)

状态计算(集合划分):j∈(0,1,2,..,i-1), 在w[i] > w[j]时,f[i] = max(f[i], f[j] + 1)。
有一个边界,若前面没有比i小的,f[i]为1(自己为结尾)。最后在找f[i]的最大值。

第二问呢,其实是第一问的另一种情况,就是求上升子序列,那么为什么呢?明明是划分,这里我也没想明白,后面看了用一下题解才知道有一个定理

Dilworth定理(Will爷万岁!!!)

Dilworth定理:偏序集的最少反链划分数等于最长链的长度

说简单点其实就是第二问相当于求最长上升子序列

那么如何证明??下面就贴一个我认为非常详细的证明

1、首先我们把这些导弹分为s组(s即为所求答案)
可以看出每一组都是一个不升子序列
2、划分完后我们在组一里找一个原序列里以组一的开头点连续的不升子串的最后一个元素,可以知道在组2中一定有一个大与它的点
(如果组二中没有的话,那么组二中最高的导弹高度必然小于这个点,而其他的高度都小于这个高度而且是递减或相等的,那么没有必要再开一个组二了,矛盾,所以不存在找不到比他大的点的情况)
3、以此类推,对于每一个k组(1<=k<n)都可以找到这样的一些点
所以把这些点连起来,就是一条上升子序列。
4、设最长上升子序列长度为l
所求上升子序列为h
那么h<=l
因为最长上升子序列任意两个不在一组内
(如果在同一个组内,则每个组的数不成为一个不生子序列,矛盾)
所以l==h

比较难理解
我们来看组数据
389 207 155 300 299 170 158 65
组一 389 207 155 65 组二 300 299 170 158
步骤一中我们一开始找到的点是1
因为如果找65不好解释,所以我们找原数列里连续的最后一个即155
组二里可以找到300比他大
所以最长上升子序列长度为2==答案
#include<bits/stdc++.h>

using namespace std;

const int maxn=100005;
int dp[maxn],a[maxn];

int main()
{
    int n,i,cnt=0,j,t;
    while(cin>>a[cnt]){
        cnt++;
    }
    int ans=0;
    for(i=0;i<cnt;i++)
    {
        dp[i]=1;
        for(j=0;j<cnt;j++)
        {
            if(a[i]<=a[j]&&i!=j)
                dp[i]=max(dp[i],dp[j]+1);
        }
        ans=max(ans,dp[i]);
    }
    cout<<ans<<endl;
    memset(dp,0,sizeof dp);
    ans=0;
    for(i=0;i<cnt;i++)
    {
        dp[i]=1;
        for(j=0;j<cnt;j++)
        {
            if(a[i]>a[j]&&i!=j)
                dp[i]=max(dp[i],dp[j]+1);
        }
        ans=max(ans,dp[i]);
    }
    cout<<ans<<endl;
    return 0;
}

那么接下来就能愉快的敲代码了,直接复制第一问改个大于号就行了,然后愉快的AC了一半...

why???

为了让大家更好地测试n方算法,本题开启spj,n方100分,nlogn200分!!!

fuck!!!

然后接着想nlogn的算法,越想头越疼,这咋能nlogn呢??裸裸的dp啊,然后看了一下题解~~

发现了一个稍微能听懂的解释,看完之后知道怎么实现了,但是还是不太明白为啥可行,晚上还是看y总吧QAQ

我们建立一个数组dp,存放前i个导弹拦截最多时,所拦截的每个导弹的高度。例如样例中所给的 389 207 155 300 299 170 158 65,我们从第一个导弹开始。第一个是389,把389存入数组dp,即dp[1]=389。定义变量s记录数组中数的个数。开始时s=1,数组中只有1个数。

第二个是207,用207与389比较,207≤389,可以拦截,所以将207存入数组。此时,数组中有2个数。

第三个是155,用155与207比较,155≤207,可以拦截,故将155存入数组。此时,数组中有3个数。

第四个是300,用300与155比较,300>155,不能拦截。但我们不能确保拦截155而不拦截300是最优的,所以不能弃之。我们在dp数组中找,找到比它小的数中最大的那个,用300替换之。注意:在找的过程中,不能从dp[1]到dp[s]逐个去找,那样又变成双重循环,会超时。由于dp数组中的数是非升序排列,所以我们可以用二分。定义l=1,r=s,每次找dp[(l+r)/2],与300比较。发现207是比它小的数中最大的一个,于是我们用300替换207。

有人会问:“这样做可行吗?”我们看,用300替换207,相当于拦截完389后直接拦截300。s表示的含义不仅仅是数组中数的个数,更是拦截导弹的数量的最大值。每来一个导弹,我们都把它放在最优的位置。用300替换207,表面上是替换,实际上是将300安排到最优的位置上。因为207现在已经没用了。一方面,300比它大,新来的导弹接到300之后肯定优于接到207之后。另一方面,300-207-155这个序列仍然存在,想要延续这个序列,我们只关心谁接到155之后,跟207没有关系了。

接下来是第五个数299,299>155,用二分查找。结果是用299替换155。

第六个数是170,此时dp的最后一个数是299,因为155被它换掉了。170≤299,所以s++,将170存入数组。此时数组中有4个数。

接下来158和65存入数组,最终s=6,故答案是6。
#include <cstdlib>
#include <iostream>
#include <algorithm>

using namespace std;

int daodan(int n, int a[])
{
	int dp[100001] ;
	dp[0] = 100000 ;
	dp[1] = a[1] ;
	int i, j, s=1, l, r, mid, ans ;
	for(i=2;i<=n;i++)
	{
		if(a[i]<=dp[s])
		{
			s++ ;
			dp[s] = a[i] ;
		}
		else
		{
			l = 0 ;
			r = s ;
			while(l<=r)
			{
				mid = (l+r)/2 ;
				if(dp[mid]>=a[i])
				{
					ans = mid ;
					l = mid + 1 ;
				}
				else
					r = mid - 1 ;
			}
			dp[ans+1] = a[i] ;
		}
	}
	return s ;
}

int main()
{
	int n=1, a[100001], s=0, i, maxs, ans=0 ;
	char c ;
	while(cin >> a[n])
	    n++ ;
	n-- ;
	cout << daodan(n,a) << endl ;
	while(s<n)
	{
		maxs = 100000 ;
		for(i=1;i<=n;i++)
		{
			if(a[i]<=maxs && a[i]>=0)
			{
				s++ ;
				maxs = a[i] ;
				a[i] = -1 ;
			}
		}
		ans++ ;
	}
	cout << ans << endl ;
	return 0 ;
}

  • 8
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值