- 01背包: 0代表不拿物品,1代表拿物品。每种物品至多只拿一件
- 完全背包: 在01背包的基础上,每种物品可拿数量由一件变多件
- 多重背包: 每种物品都有给定的数量,每种可拿物品不能超过本身固定的数量
- 混合背包: 不止一种背包(01、完全、多重,其中的两种或三种)
- 二维费用背包: 约束条件:背包重量限制,背包容量限制
- 分组背包: 已经分好组的物品,然后按01背包取解决
- 有依赖的背包: 背包添加物品时必须添加与其相依赖的物品
- 背包问题求最优方案数: 最大总价值的方案数
- 背包问题求具体方案: 最大总价值后按字典序获取具体方案(输出编号)
---------01背包--------- |
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
如:n = 4,W = 5;
物品编号 | i=0 | i=1 | i=2 | i=3 |
---|---|---|---|---|
重量 w[i] | 2 | 1 | 3 | 2 |
价值 v[i] | 3 | 2 | 4 | 3 |
二维数组实现:
递推逐项求解:后面的状态等于前面的状态总和
记dp[i+1][j]为从0到i这i+1的物品中挑选总重量不超过j的物品时的最大值;(保存最大价值)
遍历j代表找出0~j重量范围内得到最大价值的总重量,为下一次dp做铺垫;
-
如果j < w[i] 时,当前总重量区间不能选w[i]这件物品,所以dp不变,dp[i+1][j] = dp[i][j];
-
如果j >= w[i] 时,当前总重量区间可以选w[i]这件物品,所以遍历找最大,dp[i+1][j] = max(dp[i][j],dp[i][j-w[i]]+v[i]);
类比理解:我们可以把w=1,2,3,4,5当作五个容量不同的背包,0背包的状态肯定不存在,所以输出为0,每个背包在同一物品的状态都是不同的,背包状态可以分为一下四种:
- 不含有物品的背包,其容量装下当前物品。
- 不含有物品的背包,其容量可以装不下当前物品。
- 已装有物品的背包,其剩下容量可以继续装下当前物品。
- 已装有物品的背包,其剩下容量装不下当前物品。
我们都知道了每个背包的状态是四种状态之一,但是我们不需要管他们,只了解一下就可以,因为背包容量本身就限制了大于背包容量的物品肯定装不下这个物品(其背包物品的总价值还是原来,dp[i][j])。从这里我们知道背包容量,但是我们真正要考虑的是这个物品的价值是否值得装进背包(已装有物品的背包),所以我们就需要比较这个背包原来装入物品的总价值(原总价值dp[i][j])与原装入物品和新装入物品的总价值(新总价值,dp[i][j-w[i]]+v[i]),谁价值大就留下谁,得到d[i+1][j]。
获取dp[i][j]的值:
for(int i = 0;i < n;i++)
for(int j = 0;j <= W;j++){
if(j>=w[i]) dp[i+1][j] = max(dp[i][j],dp[i][j-w[i]]+v[i]);
else dp[i+1][j] = dp[i][j];
}///dp[n][W]
在这个递推式中,dp[i+1]只需计算dp[i+1]和dp[i],所以我们考虑一下滚动数组(奇偶性),提高程序的效率:
for(int i = 0;i < n;i++)
for(int j = 0;j <= W;j++){
if(j>=w[i]) dp[(i+1)&1][j] = max(dp[i&1][j],dp[i&1][j-w[i]]+v[i]);
else dp[(i+1)&1][j] = dp[i&1][j];
}///dp[n&1][W],其他背包也可以用这种方法
一维数组实现:
二维数组把装物品的背包的总价值分为(n+1)*w种状态,它不需要更新状态,它把所有状态存下来了,而一维数组把装物品的背包的总价值分为w+1种状态,每装入一个物品都要更新一次dp。
for(int i = 0;i < n;i++)
for(int j = W;j >= w[i];j--)
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);///dp[w]
---------完全背包--------- |
有n种重量和价值分别为w[i],v[i]的物品。从这些物品中挑选总重量不超过W的物品,求处挑选物品价值总和的最大值。注意:每种物品可以挑选任意多件 。
如:n = 3,W = 7;
物品编号 | i=0 | i=1 | i=2 |
---|---|---|---|
重量 w[i] | 3 | 4 | 2 |
价值 v[i] | 4 | 5 | 3 |
二维数组实现,递推求法:
存下所有的状态,还是(n+1)*w种状态。与01背包唯一不同的是在每个状态下还需要找出当前物品可以放多少个物品达到当前背包容量,并且价值最大
for(int i = 0;i < n; i++)
for(int j = 0; j <= W; j++)
for(int k = 0; k*w[i] <= j; k++)
dp[i+1][j] = max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);
///dp[n][w]
///我们很容易明白其实枚举同种物品数量是多余了,k>=1部分已经在dp[i+1][j-w[i]]中实现
///是不是还是无法明白这个多余计算包含在dp[i+1][j-w[i]]?下面再讲解
for(int i = 0;i < n;i++)
for(int j = 0;j <= W;j++){
if(j>=w[i]) dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i]);
else dp[i+1][j] = dp[i][j];
}///dp[n][W]
一维数组实现:
for(int i = 0;i < n;i++)
for(int j = w[i];j <= W;j++)
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);///dp[w]
从这里我们可以发现01背包的实现与完全背包的实现的差别只是循环顺序相反而已。我们如何理解:
参考资料:点这里
状态转移方程:dp[j] = max(dp[j],dp[j-w[i]]+v[i])
-
W→w[i]: max中的dp[j]、dp[j-w[i]]是上一循环的状态。
-
w[i]→W: max中的dp[j]、dp[j-w[i]]是本次循环的状态。我们可以发现dp[5] 为上一次循环的dp[5] 与这次循环的dp[5-2]比较,而dp[3]在这次循环中已经计算了,也就是说加了一次物品,而dp[5]这里再加一次,符合完全背包物品多件的性质。
)
---------多重背包---------
有n种物品和一个容量为W的背包。第i种物品最多有num[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
这里又多了一个限制条件,每个物品规定了可用的次数。
- 朴素算法:
for(int i = 0; i < n; i++)
for(int j = 0;j < num[i]; j--)
for(int k = W; k >= w[i]; k--)
dp[k] = max(dp[k],dp[k-w[i]]+v[i]);
- 二进制优化:我们都知道7的二进制为111,其可以分解为100、010、001三种二进制,这三个数组合成任意小于等于7的数,而每种组合都会得到不同的数。而我们可以把多重背包的一定数量的同种物品拆分为多种不同物品,从而就可以转化为01背包来求解了。
///Value[]新物品的价值
///size[]新物品的尺寸
///count新物品的个数,初始化为0
for(int i = 0; i < n; i++){
for(int j = 1; j <= num[i]; j<<=1){///j=j*2
Value[count] = j * v[i];
size[count++] = j * w[i];
num[i]-=j
}
if(num[i]>0){///num[i]为偶数时,肯定最后有一个没有扫到
Value[count] = num[i] * v[i];
size[count++] = num[i] * w[i];
}
}
for(int i = 0; i < count; i++)
for(int j = W; j >= size[i]; j--)
dp[j] = max(dp[j],dp[j - size[i]]+Value[i]);
- 单调队列优化:单调队列就是元素单调的队列。单调队列与普通队列不同的是单调队列可以从队首出队,也可以从队尾出队。
先把物品分组,按( 1,…,10)/w[i]的余数(余数为0~w[i]-1范围内)分组。如:w[i] = 3 W = 10时,3、6、9为一组,1、4、7、10为一组,2、5、8为一组。
为什么要用dp[kw[i]+j] - kv[i] ?这个状态是之前的状态,而dp[k*w[i]+j] 为本次状态。(这里还是有点懵圈…请大佬们看到的话指点一下)
///num[]物品固定数,相当于单调队列的滑动区间
///p[] 存储入队元素的下标
///k为商
///j为余数
for(int i = 1; i <= n; i++){
if(num[i]>W/w[i]) num[i] = W/w[i];
for(int j = 0; j< w[i]; j++){///j为W%w[i]的余数
int head = 1;
int tail = 1;
for(int k = 0; k*w[i]+j <= W; k++){
int temp = dp[k*w[i]+j] - k*v[i];
while(head<tail&&que[tail-1]<=temp) tail--;///当前状态价值小于待处理状态价值,出队
que[tail] = temp;
p[tail++] = k;
while(head < tail && num[i]<k-p[head]) head++;///维护队列在滑动区间内
dp[k*w[i]+j] = max(dp[k*w[i]+j],que[head]+k*v[i]);
}
}
}
---------混合背包问题--------- |
混合背包就是不止一个背包问题联合,下面是01背包、完全背包、多重背包的混合背包例子
原题:混合背包问题
import java.util.Scanner;
public class Main {
public static void main(String [] args){
int w,v,s;
int dp[] = new int[1010];
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int W = sc.nextInt();
for(int i=0; i<n;i++){
w = sc.nextInt();
v = sc.nextInt();
s = sc.nextInt();
if(s==0) {
for(int j = w; j <= W; j++)
dp[j] = Math.max(dp[j],dp[j-w]+v);
}
else {
s = Math.abs(s);///01背包问题是多重背包的特例
for(int j = 1;j <= s; j<<=1){
for(int k = W; k >= j*w; k--)
dp[k] = Math.max(dp[k],dp[k-j*w]+j*v);
s-=j;
}
if(s>0){
for(int j = W; j >= s*w; j--)
dp[j] = Math.max(dp[j],dp[j-s*w]+s*v);
}
}
}
System.out.println(dp[W]);
}
}
或者当其为完全背包时定义s为最大值,然后混合背包就转化为了多重背包。
---------二维费用背包问题--------- |
二维背包问题就是多了一个限制(重量限制),也是三种背包都存在有二维情况。
下面以01背包为例。原题来自:二维费用背包问题
import java.util.Scanner;
public class Main {///用java实现这种方法刚好到时间限制的,有点危险
public static void main(String[] args) {
int c, v, w;
int dp[][] = new int[1010][1010];
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int C = sc.nextInt();///背包容积
int W = sc.nextInt();///背包重量
for (int i = 0; i < n; i++) {
c = sc.nextInt();
w = sc.nextInt();
v = sc.nextInt();
for (int j = C; j >= c; j--) {
for (int k = W; k >= w; k--)
dp[j][k] = Math.max(dp[j][k], dp[j - c][k - w] + v);
}
}
System.out.println(dp[C][W]);
}
}
---------分组背包问题--------- |
原题来自:分组背包问题
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int num;
int w[] = new int[110];
int v[] = new int[110];
int dp[] = new int[110];
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int W = sc.nextInt();
for (int i = 0; i < n; i++) {
num = sc.nextInt();
for (int j = 0; j < num; j++){
w[j] = sc.nextInt();
v[j] = sc.nextInt();
}
for (int j = W; j >= 0; j--)
for (int k = 0; k < num; k++)///在组内找最大的
if(j>=w[k]) dp[j] = Math.max(dp[j], dp[j-w[k]] + v[k]);
}
System.out.println(dp[W]);
}
}
---------有依赖的背包问题--------- |
原题来自:有依赖的背包
import java.util.Scanner;
public class Main {
static int w[] = new int[110];
static int v[] = new int[110];
static int dp[][] = new int[110][110];///选择当前结点i体积为j的最大价值
static int n,W;
static int id;
static int e[] = new int[110];
static int Next[] = new int[110];
static int Head[] = new int[110];
public static void add(int u,int v){
e[id] = v;
Next[id] = Head[u];
Head[u] = id;
id++;
}
public static void dfs(int u){
for(int i = Head[u]; i != -1; i = Next[i]){
int son = e[i];///当前边的中点,即儿子结点
dfs(son);
for(int j = W-w[u];j >= 0; j--)///选择当前结点
for(int k = 0; k <= j; k++)///儿子结点相当于分组背包的一个组
dp[u][j] = Math.max(dp[u][j],dp[u][j-k]+dp[son][k]);
}
///加上默认选择地父节点
for(int i = W; i >= w[u]; i--) dp[u][i] = dp[u][i-w[u]] + v[u];
for(int i = 0; i < w[u]; i++) dp[u][i] = 0;
///从叶子结点开始往上做,所以如果背包容积不如当前物品的体积大,那就不能选择当前结点及其子节点
}
public static void main(String[] args) {
int root = 0;
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
W = sc.nextInt();
for(int i = 0; i<110; i++){
Head[i] = -1;
}
for(int i = 1; i <= n;i++){
int x;
w[i] = sc.nextInt();
v[i] = sc.nextInt();
x = sc.nextInt();
if(x==-1) root = i;
else add(x,i);
}
dfs(root);
System.out.println(dp[root][W]);
}
}
---------背包问题求方案数--------- |
原题来自:背包问题求方案数
以01背包为例的求最优方案:
#include<iostream>
#include<cstring>
#include<stdio.h>
#define inf 1000000
using namespace std;
const int maxn=1e4+10;
const int mod=1e9+7;
int dp[maxn];///在最大容积W背包下,最大价值
int cnt[maxn];///在最大容积W的背包下,当前最大总价值方案数
int main()
{
int n,W;
scanf("%d%d",&n,&W);
memset(dp,-inf,sizeof inf);
dp[0]=0;
cnt[0]=1;
for(int i=0;i<n;i++)
{
int v,w;
scanf("%d%d",&w,&v);
for(int j=W;j>=w;j--)
{
int now=0;
int temp=max(dp[j],dp[j-w]+v);
if(temp==dp[j]) now+=cnt[j];
if(temp==dp[j-w]+v) now+=cnt[j-w];
now%=mod;
dp[j]=temp;
cnt[j]=now;
}
}
int maxx=-1;
for(int i=0;i<=W;i++)
maxx=max(maxx,dp[i]);
int ans=0;
for(int i=0;i<=W;i++)
if(dp[i]==maxx)
ans=(ans+cnt[i])%mod;
printf("%d\n",ans);
}
---------背包问题求具体方案--------- |
普及一下01背包的二维的另外一种方法:
for(int i = n-1; i >= 0; i--)
for(int j = 0; j <= W; j++){
dp[i][j] = dp[i+1][j];
if(j >= w[i]) dp[i][j] = max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]);
}
原题:背包问题求具体问题
package lianxi;
import java.util.Scanner;
public class Main {
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int dp[][] = new int[1010][1010];
int n = sc.nextInt();
int W = sc.nextInt();
int w[] = new int[1010];
int v[] = new int[1010];
int num[] = new int[1010];
for(int i = 0;i < n;i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
}
for(int i = n-1; i>=0; i--)
for(int j = 0; j <= W; j++){
dp[i][j] = dp[i+1][j];
if(j >= w[i])
dp[i][j] = Math.max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]);
}
int j = W;
int count = 0;
for(int i = 0; i < n; i++)
if(j >=w[i]&&dp[i][j]==dp[i+1][j-w[i]]+v[i]){///等于这个状态价值肯定是最大的,因为dp[i+1][j]是没有更新
num[count++] = i;
j-=w[i];
}
for(int i = 0; i < count; i++)
System.out.print((num[i]+1)+" ");
}
}