[ TJOI 2013 ] 拯救小矮人
\(\\\)
\(Description\)
有\(N\)个人,每个人有两个属性 \(A_i,B_i\) ,现在他们要从洞里逃出去。
第 \(x\) 个人能逃出去的条件是,随意选择一些还在洞里的人\((\)重新编号为\(1...k)\),满足
注意,第 \(x\) 个人不能在这 \(k\) 个人里。求最多能跑出去多少个人。
- \(N\le 2000\ ,\ A_i,B_i,H\le10^5\)
\(\\\)
\(Solution\)
贪心和 \(DP\) 的结合,不得不说是一道好题。
首先考虑能否直接贪心。
考虑如果直接按照 \(A\) 从大到小排个序,然后强制留下来前面的一些使得满足出洞条件,然后让其他的人都出去是否是最优解。
并不是。如果把 \(A\) 比作身高,把 \(B\) 比作手长,那可能有一些身子又高手又长的人,用一些身子一般高但是手极短的人换掉,可能能逃跑的人更多。
\(\\\)
然后就有了比较厉害的 \(DP\) 解法。
首先考虑两个人 \(x,y\) 他们如果要出洞,应该以什么顺序出。
答案是 \(A+B\) 较小的那个人先出。
证明分三种情况讨论。
- 如果只能走一个,也就是说少了两人之一另一个都走不了,那谁都无所谓。
- 如果都走不了,显然顺序无关。
- 如果两个在相同的条件下都能走,那我们先让小的走,因为身子高手长的更好逃跑。
然后就可以按照 \(A+B\) 排序了。
\(\\\)
然后设状态 \(f[i]\) 表示 出去了 \(i\) 个人,剩下的人 的身高之和最多是多少。
然后状态之间的转移就代表出去了一个人。我们要保证转移合法,一定要满足:
- 一个人不能出洞多次(在多次转移里使用同一个人)
- 出洞的这个人一定在之前的集合里
然后解决这个问题的方法就比较妙了。
我们显然不能在外层枚举出去的人数,然后内层枚举出去的人,这样两个问题都无法避免。
采取一种类似背包的思路。外层枚举出去的人,内层枚举要更新的层数。
为了满足第一个限制条件,我们要像 \(01\) 背包那样更新,保证一个人只被使用一次。
为了满足第二个限制条件,我们要动态控制状态的上限。
\(\\\)
先放代码。
f[ans=0]=sum;
for(R int i=1;i<=n;++i){
for(R int j=ans;j>=0;--j)
if(f[j]+p[i].b>=m) f[j+1]=max(f[j+1],f[j]-p[i].a);
if(f[ans+1]>=0) ++ans;
}
外层循环是考虑哪个人出去,内层循环是要更新的层数,内层倒序的原理上面说过了。
转移条件挺显然的吧,就是合法就把他拿出去,然后尝试更新。
为什么一定能拿出去?因为这个位置有值,一定是从更低的转移过来,而更低的拿出去的只是当前这个人之前的人,注意体会这个 \(ans\) 的作用。
\(\\\)
\(Code\)
#include<cmath>
#include<cstdio>
#include<cctype>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define N 2010
#define R register
#define gc getchar
#define inf 2000000000
using namespace std;
inline int rd(){
int x=0; bool f=0; char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
return f?-x:x;
}
int n,m,ans,f[N];
struct prs{int a,b;}p[N];
inline bool cmp(prs x,prs y){return x.a+x.b<y.a+y.b;}
int main(){
n=rd();
for(R int i=1;i<=n;++i){
f[0]+=(p[i].a=rd());
p[i].b=rd(); f[i]=-inf;
}
m=rd();
sort(p+1,p+1+n,cmp);
for(R int i=1;i<=n;++i){
for(R int j=ans;j>=0;--j)
if(f[j]+p[i].b>=m) f[j+1]=max(f[j+1],f[j]-p[i].a);
if(f[ans+1]>=0) ++ans;
}
printf("%d\n",ans);
return 0;
}