背包DP

Posted cenqi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了背包DP相关的知识,希望对你有一定的参考价值。

背包问题是指把一定数量的物体放在一定容量的背包中,物品通常有价值和体积两种属性,求能装下背包的最大价值。

01背包

每个物体只有取与不取两种状态,对应二进制的0和1,故被称为01背包。

状态转移方程

若已知第\\(i\\)个物品的价值为\\(w_i\\),体积为\\(v_i\\),设\\(dp_i,j\\)为前\\(i\\)个物品,容量为\\(j\\)的背包所能达到的最大价值。

\\(j\\)小于\\(v_i\\)时,即当前容量无法装下该物品,得出\\(dp_i,j=dp_i-1,j\\)

\\(j\\)不小于\\(v_i\\)时,即当前容量装得下该物品,得出\\(dp_i,j=max(dp_i,j,dp_i-1,j-v_i+w_i)\\)

综上可得
​$$dp_i,j=\\begincasesdp_i-1,j&j<v_i\\\\max(dp_i,j,dp_i-1,j-v_i+w_i) &j>=v_i \\endcases$$

优化

由二维形式递推方程可知数据不断从上一行传到下一行,所以上一行之前的数据是没用的。

则可以用一维数组记录每一行的状态,用下一行的新数据覆盖上一行的旧数据。

\\(dp_j=max(dp_j,dp_j-v_i+w_i)\\)

不同于二维,一维形式下的\\(j\\)维度是逆序的,因为在一行中,每个数据是由前面的数据推得的。

如果是正序,前面的数据的就会被新数据覆盖,导致无法推出后面的的数据,但逆序就不存在这样的问题。

代码实现

二维

for(int i=1;i<=n;i++) //n为物品数量,m为背包容量
	for(int j=1;j<=m;j++) 
		dp[i][j]=dp[i-1][j];
		if(v[i]=<j) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
	

一维

for(int i=1;i<=n;i++)
	for(int j=m;j>=v[i];j--) 
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

完全背包

完全背包模型与 01 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

转态转移方程

完全背包的状态转移方程以01背包的状态转移方程十分类似,其仅有一处\\(dp_i-1,j-v_i\\)\\(dp_i,j-v_i\\)区别,而且也是通过01背包状态转移方程推导得来,具体过程略。

\\[dp_i,j=\\begincasesdp_i-1,j&j<v_i\\\\\\max(dp_i,j,dp_i,j-v_i+w_i) &j\\ge v_i \\endcases \\]

优化

完全背包的一维状态转移方程与01背包的一维状态转移方程完全相同,但是01背包的\\(j\\)维度是逆序的,而完全背包是正序的。

代码实现

二维

for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++) 
		dp[i][j]=dp[i-1][j];
		if(v[i]<=j) dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
	

一维

for(int i=1;i<=n;i++)
	for(int j=v[i];j<=m;j++)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);

暴力二维

for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
		for(int k=0;k*v[i]<=j;k++)
			dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i])

暴力二维的做法容易超时,通常使用优化二维和一维。

多重背包

多重背包也是 01 背包的一个变式。与 01 背包的区别在于每种物品有\\(k_i\\)个,而非一个。

转态转移方程

通常把多重背包拆成01背包,所以多重背包的状态转移方程和01背包一样。

优化

当数据太大时,普通做法会超时,所以可以采用二进制优化。把原来每个物品进行二进制拆分,通过这种拆分方式,可以表示任意\\(\\leq k\\)

物品的等效选择方式。然后使用 01 背包的方法解决即可。

代码实现

直接给出一维

for(int i=1;i<=n;i++) 
	int a,b,c; //c为数量
	cin>>a>>b>>c;
	for(int j=1;j<=c;j++) 
		v[cnt]=a;
		w[cnt]=b;
		cnt++;
	
 //拆成01背包

for(int i=1;i<=cnt;i++)
	for(int j=m;j>=v[i];j--)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //转态转移方程

二进制优化

for(int i=1;i<=n;i++) 
	int a,b,c;
	cin>>a>>b>>c;

    int k=1;
	while(k<=c) 
		v[cnt]=a*k;
		w[cnt]=b*k;
		c-=k;k*=2;
		cnt++;
	

	if(c>0) 
		v[cnt]=a*c;
		w[cnt]=b*c;
		cnt++;
	 //二进制拆分


for(int i=1;i<=cnt;i++) 
	for(int j=m;j>=v[i];j--)
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //状态转移方程

分组背包

分组背包指在每一组中选一个物品。

状态转移方程

\\(dp_i,j\\)为前\\(i\\)组中选,且总体积不超过\\(j\\)的所有方案的最大值,\\(v_i,k\\)表示第\\(i\\)组的第\\(k\\)个物品的体积,\\(w_i,k\\)表示第\\(i\\)组的第\\(k\\)个物体的价值。则有

​$$dp_i,j=\\begincasesdp_i-1,j&j<v_i,k\\\\max(dp_i,j,dp_i-1,j-v_i,k+w_i,k) &j\\geq v_i,k \\endcases$$

优化

因为只用到两列,所以也可已仿造01背包的套路优化。

代码实现

二维

for(int i=1;i<=n;i++)  //n为组数
	cin>>s[i];
	for(int j=1;j<=s[i];j++) 
		cin>>v[i][j]>>w[i][j];
 //s[i]为第i组的物品个数

for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++) 
		dp[i][j]=dp[i-1][j];
		for(int k=1;k<=s[i];k++)
			if(v[i][k]<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i][k]]+w[i][k]); //状态转移方程
	

一维

for(int i=1;i<=n;i++) 
	cin>>s[i];
	for(int j=1;j<=s[i];j++)
		cin>>v[i][j]>>w[i][j];


for(int i=1;i<=n;i++)
	for(int j=m;j>0;j--) //j维度逆序,枚举到0
		for(int k=1;k<=s[i];k++) 
			if(v[i][k]<=j) dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]); //要判条件,因为j是枚举到0

背包的变形

在实际使用中,通常要对背包进行变形。主要思想就是找出“容量”,“价值”和“体积”三大属性。

1.装箱问题

有一个箱子容量为 \\(V\\),同时有 \\(n\\) 个物品,每个物品有一个体积。

现在从 \\(n\\) 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。

输入格式

第一行共一个整数 \\(V\\),表示箱子容量。

第二行共一个整数 \\(n\\),表示物品总数。

接下来 \\(n\\) 行,每行有一个正整数,表示第 \\(i\\) 个物品的体积。

输出格式

共一行一个整数,表示箱子最小剩余空间。

样例输入

24
6
8
3
12
7
9
7

样例输出

0

提示

对于 \\(100\\%\\) 数据,满足 \\(0<n \\le 30\\)\\(1 \\le V \\le 20000\\)

思路

这道题每个物品的价值就是它的体积。

代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int dp[40][20010];
int w[40];

int main() 
	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);

	int v,n;
	cin>>v>>n;

	for(int i=1;i<=n;i++)
		cin>>w[i];

	for(int i=1;i<=n;i++) 
		for(int j=1;j<=v;j++) 
			dp[i][j]=dp[i-1][j];
			if(w[i]<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+w[i]);
		

	cout<<v-dp[n][v]<<"\\n";

	return 0;

2.小A点菜

uim 由于买了一些书,口袋里只剩 \\(M\\)\\((M \\le 10000)\\)

餐馆虽低端,但是菜品种类不少,有 \\(N\\)\\((N \\le 100)\\),第 \\(i\\) 种卖 \\(a_i\\)\\((a_i \\le 1000)\\)。由于是很低端的餐馆,所以每种菜只有一份。

小 A 奉行“不把钱吃光不罢休”,所以他点单一定刚好把 uim 身上所有钱花完。他想知道有多少种点菜方法。

由于小 A 肚子太饿,所以最多只能等待 \\(1\\) 秒。

输入格式

第一行是两个数字,表示 \\(N\\)\\(M\\)

第二行起 \\(N\\) 个正数 \\(a_i\\)(可以有相同的数字,每个数字均在 \\(1000\\) 以内)。

输出格式

一个正整数,表示点菜方案数,保证答案的范围在 int 之内。

样例输入

4 4
1 1 2 2

样例输出

3

思路

这道题的价值是刚好把钱花完的点菜方法。

可得出转态转移方程

\\[dp_i,j=\\begincasesdp_i-1,j&j<v_i\\\\dp_i-1,j+1 &j=v_i\\\\dp_i-1,j+dp_i-1,j-v_i &j>v_i\\endcases \\]

代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int dp[110][10010];
int v[110];

int main() 

	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);

	int n,m;
	cin>>n>>m;

	for(int i=1;i<=n;i++)
		cin>>v[i];

	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) 
		    dp[i][j]=dp[i-1][j];
			if(v[i]==j) dp[i][j]+=1;
			if(v[i]<j) dp[i][j]+=dp[i-1][j-v[i]];
		

	cout<<dp[n][m]<<"\\n";

	return 0;

3.五倍经验日

现在乐斗有活动了!每打一个人可以获得 5 倍经验!absi2011 却无奈的看着那一些比他等级高的好友,想着能否把他们干掉。干掉能拿不少经验的。

现在 absi2011 拿出了 \\(x\\) 个迷你装药物(嗑药打人可耻…),准备开始与那些人打了。

由于迷你装药物每个只能用一次,所以 absi2011 要谨慎的使用这些药。悲剧的是,用药量没达到最少打败该人所需的属性药药量,则打这个人必输。例如他用 \\(2\\) 个药去打别人,别人却表明 \\(3\\) 个药才能打过,那么相当于你输了并且这两个属性药浪费了。

现在有 \\(n\\) 个好友,给定失败时可获得的经验、胜利时可获得的经验,打败他至少需要的药量。

要求求出最大经验 \\(s\\),输出 \\(5s\\)

输入格式

第一行两个数,\\(n\\)\\(x\\)

后面 \\(n\\) 行每行三个数,分别表示失败时获得的经验 \\(\\mathitlose_i\\),胜利时获得的经验 \\(\\mathitwin_i\\) 和打过要至少使用的药数量 \\(\\mathituse_i\\)

输出格式

一个整数,最多获得的经验的五倍。

样例输入

6 8
21 52 1
21 70 5
21 48 2
14 38 3
14 36 1
14 36 2

样例输出

1060

提示

【数据范围】

  • 对于 \\(10\\%\\) 的数据,保证 \\(x=0\\)
  • 对于 \\(30\\%\\) 的数据,保证 \\(0\\le n\\le 10\\)\\(0\\le x\\le 20\\)
  • 对于 \\(60\\%\\) 的数据,保证 \\(0\\le n,x\\le 100\\)\\(10<lose_i,win_i\\le 100\\)\\(0\\le use_i\\le 5\\)
  • 对于 \\(100\\%\\) 的数据,保证 \\(0\\le n,x\\le 10^3\\)\\(0<lose_i\\le win_i\\le 10^6\\)\\(0\\le use_i\\le 10^3\\)

思路

这道题的问题在于没打过也有“价值”可加,所以在基本的状态转移方程后,还要加一个不能打过的状态转移方程。

代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int dp[1010];
int l[1010],w[1010],u[1010];

int main() 
	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);

	int n,x;
	cin>>n>>x;

	for(int i=1;i<=n;i++)
		cin>>l[i]>>w[i]>>u[i];

	for(int i=1;i<=n;i++) 
		for(int j=x;j>=u[i];j--)
			dp[j]=max(dp[j]+l[i],dp[j-u[i]]+w[i]);
		for(int j=u[i]-1;j>=0;j--)
			dp[j]+=l[i]; //没打过的状态转移方程
	

	cout<<5ll*dp[x]<<"\\n";

	return 0;

4.L国的战斗之间谍

L国即将与I国发动战争!!

俗话说的好:“知己知彼,百战不殆”。L国的指挥官想派出间谍前往I国,于是,选人工作就落到了你身上。

你现在有N个人选,每个人都有这样一些数据:A(能得到多少资料)、B(伪装能力有多差)、C(要多少工资)。已知敌人的探查间谍能力为M(即去的所有人B的和要小于等于M)和手头有X元钱,请问能拿到多少资料?

输入格式

\\(N\\) \\(M\\) \\(X\\)

\\(A_1\\) \\(B_1\\) \\(C_1\\)

\\(A_2\\) \\(B_2\\) \\(C_2\\)

………………

\\(A_n\\) \\(B_n\\) \\(C_n\\)

输出格式

能得到的资料总数

样例输入

3 10 12
10 1 11
1 9 1
7 10 12

样例输出

11

提示

【数据范围】

1≤n≤100,1≤m≤1000, 1≤x≤1000

思路

这道题有两个“价值”,因此也要多一个维度。

代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int dp[1010][1010];
int a[110],b[110],c[110];

int main() 
	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);

	int n,m,x;
	cin>>n>>m>>x;

	for(int i=1;i<=n;i++)
		cin>>a[i]>>b[i]>>c[i];

	for(int i=1;i<=n;i++) 
		for(int j=m;j>=b[i];j--)
			for(int k=x;k>=c[i];k--)
				dp[j][k]=max(dp[j][k],dp[j-b[i]][k-c[i]]+a[i]);

	cout<<dp[m][x]<<"\\n";

	return 0;

5.A+B Problem plus

给定一个正整数 \\(n\\),求将其分解成若干个素数之和的方案总数。

输入格式

一行一个正整数 \\(n\\)

输出格式

一行一个整数表示方案总数。

样例输入

7

样例输出

3

提示

【样例解释】存在如下三种方案:

  • \\(7=7\\)
  • \\(7=2+5\\)
  • \\(7=2+2+3\\)

【数据范围】

  • 对于 \\(30\\%\\) 的数据 \\(1\\le n\\le 10\\)
  • 对于 \\(100\\%\\) 的数据,\\(1\\le n\\le 10^3\\)

思路

这道题的物品数量为\\(n\\)以前的质数的数量,体积为\\(n\\),价值为\\(dp_j\\)

代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int N=1e6+10;

ll dp[1010];
int p[N],vis[N];

int eular_sieve(int n) 
	int cnt=0;
	for(int i=2;i<=n;i++) 
		if(!vis[i]) p[cnt++]=i;
		for(int j=0;j<cnt&&p[j]<=n;j++) 
			vis[i*p[j]]=1;
			if(i%p[j]==0) break;
		
	
	return cnt;


int main() 
	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);

	int n;
	cin>>n;
	int cnt=eular_sieve(n);

	dp[0]=1;
	for(int i=0;i<cnt;i++)
		for(int j=p[i];j<=n;j++) 
			dp[j]=max(dp[j],dp[j-p[i]]+dp[j]);
		

	cout<<dp[n]<<"\\n";

	return 0;

背包dp(完全)

http://poj.org/problem?id=3181

高精+背包

完全背包求方案数则只需dp[ j ] + = dp[ j - w[ i ] ]即可;任意一种背包方案计算都只需将max变为+;

这道题要注意,求方案数一般有很多,不mod,就自己看看数据是否超内存;

dp[ i ]存放前18位;dp[ i ]存放后18位;

对于前18位 dp[ j ]= dp[ j ] + dp [ j-w[ i ] ] + ( dp1[ j ]+dp1[ j-w[ i ] ) / inf

 前18位相加,在加上后18位相加时产生的进位;

技术图片
 1 #include<iostream>
 2 #include<string.h>
 3 using namespace std;
 4 #define int long long
 5 const int maxn=1100;
 6 __int64 inf=1;
 7 int dp[maxn],dp1[maxn];
 8 signed main()
 9 {
10     int n,k;
11     cin>>n>>k;
12     memset(dp,0,sizeof(dp));
13     memset(dp1,0,sizeof(dp1));
14     for (int i=0; i<18; i++)    inf *= 10;
15     dp1[0]=1;
16     for(int i=1;i<=k;i++)
17     {
18         for(int j=i;j<=n;j++)
19         {
20             dp[j]=dp[j]+dp[j-i]+(dp1[j]+dp1[j-i])/inf;
21             dp1[j]=(dp1[j]+dp1[j-i])%inf;
22         }
23     }
24    if(dp[n]) cout<<dp[n];
25     cout<<dp1[n]<<endl;
26 }
View Code

 

以上是关于背包DP的主要内容,如果未能解决你的问题,请参考以下文章

背包DP

动态规划专题

背包DP题单★

背包dp(完全)

hdu 2844 混合背包背包dp

DP基础总结