题解 CF1316E and UOJ495:一类结合贪心的背包问题
CF1316E Team Building
如果只选观众,则我们取\(a_i\)最大的\(k\)个人就好。如果只选运动员,则我们可以做一个简单的状压DP:设\(dp[i][mask]\)表示考虑了前\(i\)个人,\(mask\in[0,2^p)\)中的这些位置已经被占,此时的最大收益。DP复杂度\(O(n2^pp)\)。
考虑如何把这两种做法结合。
发现如果确定了选择哪些运动员,则观众一定从剩下的人中从大往小依次选前\(k\)个。
所以我们可以把人按“观众值”\(a_i\)从大到小排序。然后做上述的状压DP。比方说在前\(i-1\)个人中,已经选出了\(j\)个运动员,那么若\(i-j\leq k\),则第\(i\)个人如果不是运动员,就一定是观众;否则第\(i\)个人只能要么作为运动员,要么两个都不是。
时间复杂度\(O(n2^pp)\)。
参考代码:
//problem:CF1316E
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
namespace Fread{
const int MAXN=1<<20;
char buf[MAXN],*S,*T;
inline char getchar(){
if(S==T){
T=(S=buf)+fread(buf,1,MAXN,stdin);
if(S==T)return EOF;
}
return *S++;
}
}//namespace Fread
#ifdef ONLINE_JUDGE
#define getchar Fread::getchar
#endif
inline int read(){
int f=1,x=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline ll readll(){
ll f=1,x=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
/* ------ by:duyi ------ */ // myt天下第一
const int MAXN=1e5;
int n,p,K;
ll dp[MAXN+5][1<<7];
struct node{
int a,s[7];
bool operator<(const node &b){
return a>b.a;
}
}a[MAXN+5];
int main() {
n=read();p=read();K=read();
for(int i=1;i<=n;++i)a[i].a=read();
for(int i=1;i<=n;++i)for(int j=0;j<p;++j)a[i].s[j]=read();
sort(a+1,a+n+1);
memset(dp,0xcf,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;++i){
for(int j=0;j<(1<<p);++j){
for(int k=0;k<p;++k)if(!(j&(1<<k))){
dp[i][j|(1<<k)]=max(dp[i][j|(1<<k)],dp[i-1][j]+a[i].s[k]);
}
if(i-__builtin_popcount(j)<=K){
dp[i][j]=max(dp[i][j],dp[i-1][j]+a[i].a);
}
else dp[i][j]=max(dp[i][j],dp[i-1][j]);
}
}
cout<<dp[n][(1<<p)-1]<<endl;
return 0;
}
UOJ495 新年的促销
暴力的DP做法:
设\(dp[i][j][k][l]\)表示考虑了前\(i\)件物品,总共花了\(j\)元钱,花钱买了\(k\)袋大米,白嫖了\(l\)袋大米时获得的最大重量。统计答案时判断\(k\)和\(l\)的关系即可。
时间复杂度\(O(n^3m)\)。期望得分\(60\)分。
正解做法:
尝试挖掘题目中的性质。发现:
- 性质一:任意一袋花钱买的大米的价格,一定小于等于任意一袋白嫖的大米的价格。证明:否则我们把白嫖的比买的更便宜的那一袋大米买下来,把买的比白嫖的更贵的那一袋大米用来白嫖,总重量不变,花的钱变少了。
- 性质二:当已经确定了哪些大米是花钱买来的时,白嫖的大米一定从没买的大米中选重量最大的那几袋。正确性显然。
把所有大米按价格从小到大排序。根据性质一,考虑枚举花钱买的大米中最贵的是哪一袋。边枚举边DP。设\(dp[i][j][k]\)表示枚举了价格前\(i\)小的物品,买了\(j\)个,花了\(k\)元钱,获得的最大重量。根据性质二,赠品可以直接在后\(n-i\)个物品里选重量前若干大的。即每枚举一个\(i\),都把后面的物品按重量排个序,然后更新所有\(m\)的答案。
时间复杂度\(O(n^2m+n^2\log n)\)。
参考代码:
//problem:uoj495
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fst first
#define scd second
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
namespace Fread{
const int MAXN=1<<20;
char buf[MAXN],*S,*T;
inline char getchar(){
if(S==T){
T=(S=buf)+fread(buf,1,MAXN,stdin);
if(S==T)return EOF;
}
return *S++;
}
}
#ifdef ONLINE_JUDGE
#define getchar Fread::getchar
#endif
inline int read(){
int f=1,x=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline ll readll(){
ll f=1,x=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
/* ------ by:duyi ------ */ // dysyn1314
inline void cmax(int &x,int y){x=x<y?y:x;}
const int MAXN=300,MAXM=1000;
struct item{int w,p;}a[MAXN+5],b[MAXN+5];
bool cmp_p(item a,item b){return a.p<b.p;}
bool cmp_w(item a,item b){return a.w<b.w;}
int n,m,A,B,dp[MAXN+5][MAXN+5][MAXM+5],suf[MAXN+5],ans[MAXM+5];
int main() {
n=read();m=read();A=read();B=read();
for(int i=1;i<=n;++i)a[i].w=read();
for(int i=1;i<=n;++i)a[i].p=read();
sort(a+1,a+n+1,cmp_p);
memset(dp,0xcf,sizeof(dp));
dp[0][0][0]=0;
for(int i=1;i<=n;++i){
for(int j=0;j<=n;++j){
for(int k=0;k<=m;++k){
cmax(dp[i][j][k],dp[i-1][j][k]);
if(j>=1&&k>=a[i].p)cmax(dp[i][j][k],dp[i-1][j-1][k-a[i].p]+a[i].w);
}
}
for(int j=i+1;j<=n;++j)b[j]=a[j];
sort(b+i+1,b+n+1,cmp_w);
for(int j=n;j>=i+1;--j)suf[j]=suf[j+1]+b[j].w;
for(int j=1;j<=i;++j){
for(int k=1;k<=m;++k){
cmax(ans[k],dp[i][j][k]+suf[max(i+1,n-(j/A+j/B)+1)]);
}
}
}
for(int i=1;i<=m;++i){
cmax(ans[i],ans[i-1]);
printf("%d ",ans[i]);
}
puts("");
return 0;
}
小结与思考
当题目的条件较为复杂时,传统的背包DP往往需要增加很多维度,造成时间复杂度不够优秀。
此时就需要我们仔细发掘题目的性质,找到可以贪心的地方。把一部分条件用贪心处理。这样就大大提高了算法效率。