题解 CF1316E and UOJ495:一类结合贪心的背包问题

CF1316E Team Building

题目链接

如果只选观众,则我们取(a_i)最大的(k)个人就好。如果只选运动员,则我们可以做一个简单的状压DP:设(dp[i][mask])表示考虑了前(i)个人,(maskin[0,2^p))中的这些位置已经被占,此时的最大收益。DP复杂度(O(n2^pp))

考虑如何把这两种做法结合。

发现如果确定了选择哪些运动员,则观众一定从剩下的人中从大往小依次选前(k)个。

所以我们可以把人按“观众值”(a_i)从大到小排序。然后做上述的状压DP。比方说在前(i-1)个人中,已经选出了(j)个运动员,那么若(i-jleq 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^2log 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往往需要增加很多维度,造成时间复杂度不够优秀。

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

原文地址:https://www.cnblogs.com/dysyn1314/p/12527703.html