[Noip1999]导弹拦截 剖析

题目链接

拦截导弹(Noip1999)

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

INPUT

389 207 155 300 299 170 158 65

OUTPUT

6(最多能拦截的导弹数)
2(要拦截所有导弹最少要配备的系统数)

这是一道很经典的题目,做法也是多种多样的。
首先,第一问 O(n^2) 的方法是很容易想到的,就是求最长不升子序列的长度~
有:

$$ f[i]=\max(f[i],f[j]+1)(a[i]<=a[j] && i>j) $$

但在看第二问之前,我们先需要知道Dilworth定理。它的内容大致是:

对于一个偏序集,最少链划分等于最长反链长度。

这句话很拗口,是吧?翻译成题目意思就是说,不上升子序列的个数至少为最长上升子序列的长度(如果不理解,可以参考这位大牛的博客)
即:

$$ g[i]=\max(g[i],g[j]+1)(a[i]>a[j] && i>j) $$

下贴代码:

#include<bits/stdc++.h>
#define mod (int)(1e9+7)
using namespace std;
const int maxn=1010;
typedef long long ll;
const int inf=1e9;

int up[maxn],down[maxn],a[maxn];
int max1,max2;

int main( ){
    int m,n,j,k,i,cnt=0;
    while(scanf("%d",&a[++cnt])!=EOF);cnt--;
    for(i=0;i<=cnt;i++)
    	up[i]=down[i]=1;
    for(i=2;i<=cnt;i++){
        for(j=1;j<i;j++){
            if(a[i]<=a[j] && up[i]<up[j]+1)
                max1=max(max1,(up[i]=up[j]+1));
        }
    }
    for(i=2;i<=cnt;i++){
        for(j=1;j<i;j++){
            if(a[j]<a[i] && down[j]+1>down[i])
                max2=max(max2,(down[i]=down[j]+1));
        }
    }
    printf("%d\n%d\n",max1,max2);
    return 0;
}

当然,我们只讲这么一点肯定不够是吧
如果某个丧心病狂的出题人把导弹个数出成100000个,那我们的 O(n^2) Dp肯定就过不去了
这时候我们发现只有 O(n log n) 与 O(n) 做法才能A掉。而显然后者是不太现实的,于是我们考虑用一个队列(或栈)来二分维护序列的长度
比如做第一问的时候,可以先建立一个空队列,插入数的时候,只有两种情况:
1.如果这个数比队列末尾的数还小,则直接将其入队
2.另外的,就找到第一个比这个数小的数,并将其替换为这个数,因为它更具有潜力(这个过程借助二分实现)
下贴代码:

#include <bits/stdc++.h>
using namespace std;

bool read(int& k){
    int Base=1;char Ch=getchar();
    while(!isdigit(Ch)){if(Ch=='-')Base=-1;if(Ch==EOF)return false;Ch=getchar();}
    while(isdigit(Ch)){k=(k<<1)+(k<<3)+(Ch^'0');Ch=getchar();}
    return true;
}

const int maxn=300010;
int a[maxn],qx[maxn],qy[maxn];

int main(){
    int i,j,k,m,n=0;
    while(read(a[++n]));n--; 
    int lenx=0,leny=0;
    qx[0]=1e9;
    for(i=1;i<=n;i++){
        if(a[i]<=qx[lenx]){qx[++lenx]=a[i];continue;}
        int head=1,tail=lenx;
        while(head<tail){
            int mid=((head+tail)>>1);
            if(a[i]<=qx[mid])head=mid+1;
            else tail=mid;
        }
        qx[head]=a[i];
    }
    qy[0]=-1e9;
    for(i=1;i<=n;i++){    
        if(a[i]>qy[leny]){qy[++leny]=a[i];continue;}
        int head=1,tail=leny;
        while(head<tail){
            int mid=(head+tail)>>1;
            if(a[i]>qy[mid])head=mid+1;
            else tail=mid;
        }
        qy[head]=a[i];
    }
    printf("%d\n%d\n",lenx,leny);
    return 0;
}

个人喜欢手打队列,不过其实stl的lowerbound也是可以的(主要是不会啊QAQ)
好吧,其实第二问还有一个奇妙的做法,但不知道二分图的孩子们可以先略过了
上代码

#include<bits/stdc++.h>
#define mod (int)(1e9+7)
#define mscheck(s) if(s=='-')Base=-1;else if(s==EOF)return false;
using namespace std;
typedef long long ll;
const int inf=1e9;

const int maxn=1010;
int a[maxn],f[maxn],p[maxn],ansx,ansy;
int beg[maxn*maxn*2],nex[maxn*maxn*2],to[maxn*maxn*2],e;
int vis[maxn],link[maxn],ans,now;

bool read(int& Value){
    int Base=1;char Ch=getchar();
    for(;!isdigit(Ch);Ch=getchar())mscheck(Ch);
    for(;isdigit(Ch);Ch=getchar())Value=Value*10+(Ch^'0');
    Value*=Base;return true;
}

void add(int x,int y){
    to[++e]=y;
    nex[e]=beg[x];
    beg[x]=e;
}

bool dfs(int x){
    for(int i=beg[x];i;i=nex[i]){
        int y=to[i];
        if(vis[y]!=now){
            vis[y]=now;
            if(!link[y] || dfs(link[y])){
                link[y]=x;
                return true;
            }
        }
    }
    return false;
}

int main( ){
    int m,n=0,j,k,i;
    while(read(a[++n]));n--;
    for(i=1;i<=n;i++){
        for(j=1;j<i;j++){
            if(a[j]>=a[i]){
                f[i]=max(f[i],f[j]+1);
                ansx=max(ansx,f[i]);
                add(j,i);
            }
        }
    }
    for(i=1;i<=n;i++){
        now++;
        ansy+=dfs(i);
    }
    printf("%d %d",ansx+1,n-ansy);
    return 0;
}

这里第一问的做法与 O(n^2) 的没有区别,但看第二问之前我们先看一下二分图的一个重要定理:

DAG图的最小路径覆盖数=节点数-二分图的最大匹配数

大意就是如果要用最少的简单边覆盖整个图,那么这些边的个数就是节点数-二分图最大匹配数
这个其实很好理解,首先我们建立一个二分图,满足A集合和B集合之间没有边,则定理显然成立。
这时候,我们找到两个点ai,bi并连接一条匹配边(ai,bi),则路径覆盖数就比原先少了一个
这时继续推广,不难发现每增加一条A、B间的匹配边,路径覆盖数就会减少一个;匹配边数不能再增加的时候,路径覆盖数正好为一。
也就是说二分图的每一条匹配边,都与该二分图的一条DAG图的覆盖路径对应

所以我们可以构造二分图,满足:
如果

$$ a[j]<a[i] && j<i \ 就连上一条边 <j,i> $$

最后求一求导弹数-二分图匹配数就可以了

最后再看一下一个类似题目

题目链接

[usaco]低价购买

Description

“低价购买”这条建议是在奶牛股票市场取得成功的一半规则。要想被认为是伟大的投资者,你必须遵循以下的问题建议:“低价购买;再低价购买”。每次你购买一支股票,你必须用低于你上次购买它的价格购买它。买的次数越多越好!你的目标是在遵循以上建议的前提下,求你最多能购买股票的次数。你将被给出一段时间内一支股票每天的出售价(2^16范围内的正整数),你可以选择在哪些天购买这支股票。每次购买都必须遵循“低价购买;再低价购买”的原则。写一个程序计算最大购买次数。
这里是某支股票的价格清单:
    日期 1 2 3 4 5 6 7 8 9 10 11 12
    价格 68 69 54 64 68 64 70 67 78 62 98 87
最优秀的投资者可以购买最多4次股票,可行方案中的一种是: 
    日期 2 5 6 10
    价格 69 68 64 62

Input

  第1行: N (1 <= N <= 5000),股票发行天数
  第2行: N个数,是每天的股票价格。

Output

输出文件仅一行包含两个数:最大购买次数和拥有最大购买次数的方案数(<=2^31)当二种方案“看起来一样”时(就是说它们构成的价格队列一样的时候),这2种方案被认为是相同的。

Sample Input

12
68 69 54 64 68 64 70 67 78 62 98 87

Sample output

4 2

题目第一问与导弹拦截一模一样,第二问需要一点思考。
假设价格队列长度为 f[i],方案数为 g[i],递推式就为

$$ g[i]=g[i]+g[j] (f[i]==f[j+1] && price[i]!=price[j])$$

又因为如果价格队列相同就算相同队列,所以要完善一下:

$$ g[j]=0 (price[i]price[j] && f[i]f[j]) $$

代码就是:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5010;
const int inf=1e9;
int f[maxn];
int ans,imax,a[maxn],g[maxn];

int read(){
    int Value=0,Base=1;char Ch=getchar();
    for(;!isdigit(Ch);Ch=getchar())if(Ch=='-')Base=-1;
    for(;isdigit(Ch);Ch=getchar())Value=(Value*10)+(Ch^'0');
    return Value*Base;
}

int main( ){
    int m,n,j,k,i;
    n=read();
    for(i=1;i<=n;i++)
        a[i]=read();
	ans=-1e9;
    for(i=1;i<=n;i++)f[i]=1;
    for(i=1;i<=n;i++){
        for(j=1;j<i;j++){
            if(a[j]>a[i]){
                if(f[i]<f[j]+1){
                    ans=max(ans,f[j]+1);
                    f[i]=f[j]+1;
                }
            }
        }
    }
	if(ans==-1e9)ans=1;
	for(i=1;i<=n;i++){
		if(f[i]==1)g[i]=1;
		for(j=1;j<i;j++){
			if(f[i]==f[j] && a[i]==a[j])g[j]=0;
			if(f[i]==f[j]+1 && a[i]<a[j])g[i]+=g[j];
		}
	}
    for(i=1;i<=n;i++)
		if(f[i]==ans)
			imax+=g[i];
    printf("%d %d\n",ans,imax);
    return 0;
}
posted @ 2017-09-30 17:49  ABCDXYZ  阅读(801)  评论(0编辑  收藏  举报
www.cnblogs.com/ABCDXYZ %%%[yyc](https://yycjm.github.io/Games/hextris/hextris.html)