题解 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往往需要增加很多维度,造成时间复杂度不够优秀。

此时就需要我们仔细发掘题目的性质,找到可以贪心的地方。把一部分条件用贪心处理。这样就大大提高了算法效率。

posted @ 2020-03-19 22:26  duyiblue  阅读(210)  评论(1编辑  收藏  举报