[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;
}