背包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背包状态转移方程推导得来,具体过程略。
优化
完全背包的一维状态转移方程与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
思路
这道题的价值是刚好把钱花完的点菜方法。
可得出转态转移方程
代码
#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 }
以上是关于背包DP的主要内容,如果未能解决你的问题,请参考以下文章