20260722 - 背包 总结

前言

不是老师你加两个 2200 和 2300 的防 AK 是想干啥,我们又做不出来的。。

【数据删除】:这是第几次上背包了?

背包

我不想讲了,大概就是背包背包背包,要看去看这个,挺详细的。

A - Fair Division

首先求出 \(sum = \sum a\),如果 \(sum\) 是奇数那么一切都完蛋了(不可能平均分),否则写个 bool 类型的 DP 就做完了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 205;
int T,n,a[N],sum;bool dp[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	T=read();
	while(T--){
		n=read(),sum=0;
		for(int i=1;i<=n;i++)a[i]=read(),sum+=a[i];
		for(int i=0;i<=sum;i++)dp[i]=0;
		if(sum&1){cout<<"NO\n";continue;}
		dp[0]=1;
		for(int i=1;i<=n;i++)
		    for(int j=a[i];j<=sum;j++)dp[j]|=dp[j-a[i]];
		if(dp[sum/2])cout<<"YES\n";
		else cout<<"NO\n";
	}
    return 0;
}

B - 开心的金明

简单背包,这个重要度起不到什么作用,主要就是给你固定价值的,有容量限制、有代价也有价值(收益),那就是最普通的 0/1 背包了,做就完了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
int n,m,v[30],w[30],dp[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++)v[i]=read(),w[i]=read();
	for(int i=1;i<=m;i++)
		for(int j=n;j>=v[i];j--)
		    dp[j]=max(dp[j],dp[j-v[i]]+v[i]*w[i]);
	cout<<dp[n]<<"\n";
    return 0;
}

C - 总分 Score Inflation

我们发现题目是个完全背包,然后没然后了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e4+5;
int n,m,p[N],t[N],dp[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++)p[i]=read(),t[i]=read();
	for(int i=1;i<=n;i++)
		for(int j=t[i];j<=m;j++)
		    dp[j]=max(dp[j],dp[j-t[i]]+p[i]);
	cout<<dp[m]<<"\n";
    return 0;
}

D - Money in Hand

多重背包的模板题,但是因为 \(n\)\(b_i\) 都很小,所以根本不需要用到多重背包的拆进制压缩物品个数,直接无脑塞 \(b_i\) 个也是没任何问题的。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 3005;
const int M = 1e4+5;
int k,n,m,a[N];bool dp[M];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	k=read(),m=read();n=0;
	for(int i=1;i<=k;i++){
		int x=read(),y=read();
		while(y--)a[++n]=x;
	}
	dp[0]=1;
	for(int i=1;i<=n;i++)
		for(int j=m;j>=a[i];j--)dp[j]|=dp[j-a[i]];
	if(dp[m])cout<<"Yes\n";
	else cout<<"No\n";
    return 0;
}

E - 榨取kkksc03

多维背包,大概就是你定义状态的时候需要定义当前剩余的钱数和时间两个维度,转移什么的都照常,注意是 0/1 背包(滚动数组的话不要错写成完全背包),枚举的时候要倒着来。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
int n,m,T,dp[205][205];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	n=read(),m=read(),T=read();
	for(int i=1;i<=n;i++){
		int v=read(),t=read();
		for(int x=m;x>=v;x--)
		    for(int y=T;y>=t;y--)
		        dp[x][y]=max(dp[x][y],dp[x-v][y-t]+1);
	}cout<<dp[m][T]<<"\n";
    return 0;
}

F - Cutting

显然,在保证切割操作都合法的情况下,能切割的位置一定是固定的一些位置,也就是前缀的奇数个数与偶数个数相同的位置。

那么你直接按照这个思路去做就行了,\(dp_i\) 表示切割 \(i\) 次的最小代价(滚动数组),最后就是找到最大的能使 \(dp_{ans} \le B\)\(ans\) 即为答案。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 105;
const int INF = 0x3f3f3f3f;
int n,B,a[N],dp[N],Ans;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void upd(int &x,int y){x=min(x,y);return;}
int main(){
	n=read(),B=read();
	for(int i=1;i<=n;i++)a[i]=read();
	if(n&1){cout<<"-1\n";return 0;}
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	int c0=0,c1=0;
	for(int i=1;i<=n;i++){
		if(a[i]&1)c1++;else c0++;
		//cout<<c0<<","<<c1<<"!\n";
		for(int q=i-1;q>=0;q--){
			if(dp[q]>=INF)continue;
			int nq=q+(c0==c1&&i<n),val=0;
		    if(c0==c1&&i<n)val=abs(a[i+1]-a[i]);
		    dp[nq]=min(dp[nq],dp[q]+val);
		}
		//for(int x=0;x<=i;x++)cout<<(dp[x]==INF?-1:dp[x])<<" ";cout<<"\n";
	}
	for(int i=0;i<=n;i++)if(dp[i]<=B)Ans=i;
	cout<<Ans<<"\n";
    return 0;
}

G - Robot Arms 2

发现题目的要求事实上就是,将所有 \(a\) 按下标奇偶性分成 \(1\) 组和 \(2\) 组,并对每个数分配一个 \(+/-\) 符号(\(a_1\) 强制符号为 \(+\)),对其每个数乘上其符号后的结果求和,求是否存在一种符号的分配方案使 \(1\) 组和为 \(X\) 且二组和为 \(Y\)

这两个东西是独立的,我们可以分开做 DP,bool 类型的 DP。最后判一下是否都有就行。

实现上可以用 bitset 优化一下。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e3+5;
const int M = 2e4+5;
const int pian = 10000;
int n,a[N],X,Y;
bitset<M> dp,f;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	n=read(),X=read(),Y=read();
	X+=pian,Y+=pian;
	dp[pian]=1,f[pian]=1;
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=n;i++){
		if(i&1)
		    if(i>1)dp=(dp<<a[i])|(dp>>a[i]);
		    else dp=dp<<a[i];
	    else f=(f<<a[i])|(f>>a[i]);
	}
	if(dp[X]&&f[Y])cout<<"Yes\n";
	else cout<<"No\n";
    return 0;
}

H - Buns

我最开始想复杂了,以为一种馅的包子还有不同形式的,虽然那样也能做。

还是一个类似多重背包的东西,没有馅的包子看似是无限个,其实数量也是有限制的(因为面团数量是有限的);而有馅的包子就更加了,它可以根据这种馅的数量算出这种馅的包子能做几个,扔到一起。这里也不需要多重背包的优化,无脑扔进数组就行了,并不会炸。

然后跑普通的 0/1 背包就结束了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2005;
int k,m,n=0,a[N],b[N],dp[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	k=read(),m=read();
	int x_0=read(),y_0=read();
	for(int j=1;j<=k/x_0;j++)
	    ++n,a[n]=x_0,b[n]=y_0;
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),z=read(),w=read();
		for(int j=1;j<=x/y&&z*j<=k;j++)
		    ++n,a[n]=z,b[n]=w;
	}
	for(int i=1;i<=n;i++)
		for(int j=k;j>=a[i];j--)
		    dp[j]=max(dp[j],dp[j-a[i]]+b[i]);
	cout<<dp[k]<<"\n";
    return 0;
}

I - 金明的预算方案

可以把这个东西当做一个分组背包,一套主件和附件就是一个组,这个组内具体会有四种情况:只选主件,选主件和第一个附件,选主件和第二个附件,全选。(当然需要考虑没有附件和只有一个附件的情况。)

前面的处理需要一点耐心,其他的就是板子分组背包了。

至于我为什么做了这么久,呃呃呃,这是因为我忘记分组背包怎么做的了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 4e4+5;
struct node{int v,p,q;}a[105];
int n,m,dp[N],f[N];
vector<pii> g[105],h[105];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void swap_dpf(){
	for(int i=0;i<=m;i++)f[i]=dp[i];return;
}
int main(){
	m=read(),n=read();
	for(int i=1;i<=n;i++){
		a[i].v=read(),a[i].p=read(),a[i].q=read();
		if(!a[i].q)g[i].pb(make_pair(a[i].v,a[i].v*a[i].p));
	}
	for(int i=1;i<=n;i++)if(a[i].q)
	    g[a[i].q].pb({a[i].v,a[i].v*a[i].p});
	for(int i=1;i<=n;i++){
		if(g[i].size()<=1){
			h[i].swap(g[i]);continue;
		}
		if(g[i].size()==2){
			h[i].pb(g[i][0]);
			h[i].pb(make_pair(g[i][0].fr+g[i][1].fr,g[i][0].se+g[i][1].se));
		}
		if(g[i].size()==3){
			h[i].pb(g[i][0]);
			h[i].pb(make_pair(g[i][0].fr+g[i][1].fr,g[i][0].se+g[i][1].se));
			h[i].pb(make_pair(g[i][0].fr+g[i][2].fr,g[i][0].se+g[i][2].se));
			h[i].pb(make_pair(g[i][0].fr+g[i][1].fr+g[i][2].fr,g[i][0].se+g[i][1].se+g[i][2].se));
		}
	}
	for(int i=1;i<=n;i++){
		swap_dpf();
		for(int j=m;j>=0;j--){
			for(int x=0;x<h[i].size();x++){
				int u=h[i][x].fr,v=h[i][x].se;
				if(u<=j)dp[j]=max(dp[j],f[j-u]+v);
			}
		}
	}
	cout<<dp[m]<<"\n";
    return 0;
}
/*

主件和附件……分组吗?
感觉可以用分组背包的思想去做啊
开个 vector 搞搞就行 

*/

J - 货币系统

很简单的题目。

首先你跑个 DP 求出在 \(max a_i\) 以内的每种货币分别有多少种情况能表示出来,记作 \(dp_x\);那么一个 \(a_i\) 能不能被删掉取决于 \(dp_{a_i}\) 的值,如果是 \(1\) 说明只能由自己构成,不能删去,否则一定可以删去。统计一下个数就行。

做 DP 的时候为了防止方案数太大爆炸,可以考虑把上限锁死成 \(3\),这样就不用担心越界变成负数什么的了。当然对某个数取模也应该是可以的,但要小心取模后变成 \(1\) 误判的情况。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 3e4+5;
int T,n,mxa,a[105],ans;int dp[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int add(int x,int y){return min(3,x+y);}
int main(){
	T=read();
	while(T--){
		n=read(),mxa=0;
		for(int i=1;i<=n;i++)
			a[i]=read(),mxa=max(mxa,a[i]);
		for(int i=0;i<=mxa;i++)dp[i]=0;
		dp[0]=1,ans=0;
		for(int i=1;i<=n;i++)
		    for(int j=a[i];j<=mxa;j++)dp[j]=add(dp[j],dp[j-a[i]]);
		for(int i=1;i<=n;i++)if(dp[a[i]]==1)ans++;
		cout<<ans<<"\n";
	}
    return 0;
}

K - Birds

【数据删除】:这个 2200 好简单啊。
:你自己切出来了吗就叫。

难点在于状态设计。

仔细读题,发现 \(B\)\(X\) 都和 \(i\) 没有关系,这里应该和状态挂钩;然后又有一个限制是 \(\sum c_i \le 10^4\),这个也是很小的。

我们考虑定义这样一个状态:\(dp_{i,j}\) 表示当前考虑到第 \(i\) 棵树,且已经召唤了 \(j\) 只小鸟的情况下,剩余的最多魔法值。诶诶,魔法值上限在哪?因为 \(B\) 是常量,所以上限就是 \(W + Bj\) 啦。

转移是简单的,然后就做完了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1005;
const int M = 1e4+5;
LL n,W,B,X,c[N],cst[N],sum[N],dp[N][M],Ans;
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
	n=read(),W=read(),B=read(),X=read();
	for(int i=1;i<=n;i++)
	    c[i]=read(),sum[i]=sum[i-1]+c[i];
	for(int i=1;i<=n;i++)cst[i]=read();
	memset(dp,-1,sizeof(dp));
	dp[0][0]=W;
	for(int i=1;i<=n;i++)
	    for(int x=0;x<=c[i];x++)
			for(int j=x;j<=sum[i];j++){
				if(dp[i-1][j-x]<x*cst[i])continue;
				dp[i][j]=max(dp[i][j],min(W+j*B,dp[i-1][j-x]-x*cst[i]+X));
			}
	for(LL j=0;j<=sum[n];j++)
	    if(dp[n][j]!=-1)Ans=max(Ans,j);
	cout<<Ans<<"\n";
    return 0;
}

L - Knapsack

挺……巧妙的一个题。

\(8\) 很奇怪啊,\(8\) 是个啥??这肯定是突破口,但是咋突破呢?

背包容量太恐怖了没法直接做,怎么办呢?我们考虑将物品进行一些分组,组的大小尽可能不要太大,并且每个组内部能快速计算答案,取余后的部分就很小可以直接 DP。

唔,要求挺高。但是,确实可以达到,我们取组大小为 \(\text{lcm}(1,2,\dots,8) = 840\)。这个大小是非常合适的,因为这个组肯定能用任何一个数取满,也可以和其他组完美衔接。所以就可以贪心,尽可能取多,这样这个背包总重量(不是限重)就会更大。而对于这个组剩下的部分,就只有 \(840 \times 8\) 这么点了,不超过 \(7000\),很小,我们直接 DP 做就行了。

那么就是具体的 DP 了,这个还是比较简单的,我们定义 \(dp_{i,j}\) 表示前 \(i\) 种物品的“余数”(剩下来的部分)为 \(j\) 时能取到的最多组数。转移就枚举一下这个 \(i\) 的余数是多少,然后将剩下的部分拿去凑组就行了。

答案是好求的。然后就做完了。

戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 7005;
const int up = 840*8;
LL W,cnt[10],dp[10][N],Ans;
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    W=read();
    for(int i=1;i<=8;i++)cnt[i]=read();
    memset(dp,-1,sizeof(dp));
    dp[0][0]=0;
    for(int i=1;i<=8;i++)
        for(int j=0;j<=up;j++){
            if(dp[i-1][j]<0)continue;
            int sz=min(cnt[i],840ll/i);
            for(int x=0;x<=sz;x++)if(j+x*i<=up)
                dp[i][j+x*i]=max(dp[i][j+x*i],dp[i-1][j]+(cnt[i]-x)/(840/i));
        }
    Ans=0;
    for(int j=0;j<=up;j++){
        if(j>W||dp[8][j]<0)continue;
        Ans=max(Ans,j+min(dp[8][j],(W-j)/840)*840);
    }
    cout<<Ans<<"\n";
    return 0;
}

总结

背包好吃。

背包好难。

posted @ 2026-07-22 16:39  嘎嘎喵  阅读(16)  评论(0)    收藏  举报