背包问题详解
背包问题核心思想为DP(Dynamic Programming),本文基于核心思路实现逐步推导.
最大价值简称:mv
01背包
例题描述:
有 N 件物品和一个容量为 M 的背包。第 i 件物品的重量是 Wi,价值是 Di。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
输入:
第一行:物品个数 N 和背包大小 M。
第二行至第 N+1 行:第 i 个物品的重量 Wi 和价值 Di。
实验数据:
4 6 1 4 2 6 3 12 2 7
预测结果
23
题目地址:P2871 [USACO07DEC] Charm Bracelet S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
基础假设:dp[i][j]
表示在第i
个元素并且剩余容量为j
时的最大价值
满足最优子结构和无后效性
基于基础假设,尝试得出2维dp
表
采用递推实现
物品\容量 | j=0 | j=1 | j=2 | j=3 | j=4 | j=5 | j=6 |
---|---|---|---|---|---|---|---|
i=0,w=0,v=0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
i=1,w=1,v=4 | 0 | 4 | 4 | 4 | 4 | 4 | 4 |
i=2,w=2,v=6 | 0 | 4 | 6 | 10 | 10 | 10 | 10 |
i=3,w=3,v=12 | 0 | 4 | 6 | 12 | 16 | 18 | 22 |
i=4,w=2,v=7 | 0 | 4 | 7 | 12 | 16 | 19 | 23 |
注: 1.设置dp[i]
设置i=0
目的是防止dp[i-1]
越界
2.表中数据表示此时的mv
打表代码:
@Test
void contextLoads() {
int n = 4;
int m = 6;
int[] w = {0,1,2,3,2};
int[] v = {0,4,6,12,7};
int [][] dp = new int[5][7];
for (int i = 1; i < 5; i++) {
for (int j = 0; j < 7 ; j++) {
if(j < w[i]){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]] + v[i]);
}
}
}
for (int i = 0; i < 5; i++) {
for (int i1 = 0; i1 < 7; i1++) {
System.out.print(dp[i][i1]);
}
System.out.println();
}
}
解释表格得出过程:
-
j=0或i=0时
,表示容量为0,此时mv
显然为0 -
i=1且j=1时
,容量j 等于w ,不拿mv=0
,拿取mv=4
,选择拿取 -
i =1且j>1时
,mv=4
恒成立 -
i=2且j=1时
,j<w
,无法拿取,mv = 4
-
i=2且j=2时
,j=w
,满足拿取条件:不拿,mv = 4
,拿取,mv = dp[1][2-2]+v[2]=0+6=6
,故mv=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) =6
-
i=2且j=3时
,j>=w
,满足拿取条件:不拿,mv=4
,拿取mv =dp[1][3-2]+v[2]=4+6 =10
,故mv = 10
-
之后情况依次递推
注:当i=1时,比较依然存在只是无意义
由过程推导得出:打表时的核心方程:当j > w[i]
,dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
当j<w[i]
,dp[i][j]=dp[i-1][j];
核心流程解释完毕
优化:
显然建表时,表大小约为(n+1)(m+1)
,可能造成MLE
理论优化思路:
i=n行的数据仅和i=n-1有关,考虑压缩数组(或者滚动数组),此时数组变为一维,核心方程变为j>=w[i]
,dp[j]=max(dp[j],dp[j-w[i]]+v[i])
,j<w
,dp[j]=dp[j]
,显然可以合并为 j=w[i]
,dp[j]=max(dp[j],dp[j-w[i]]+v[i])
代码优化思路:
直接将j>=w[i]
提升至循环体内,减少比较次数
注意点:
对于j的遍历优化后需要从后往前,因为比较时需要dp[j-w[i]]
(也就是dp[i-1][j-w[i]]
),从前向后会导致数据覆盖
优化后的测试代码:
@Test
void contextLoads2() {
int n = 4;
int m = 6;
int[] w = {0,1,2,3,2};
int[] v = {0,4,6,12,7};
int[] dp = new int[7];
for (int i = 1; i < 5; i++) {
for (int j = 6; j >=w[i] ; j--) {
dp[j] = Math.max(dp[j],dp[j-w[i]] + v[i]);
}
for (int t = 0; t < dp.length; t++) {
System.out.print(dp[t]);
}
System.out.println();
}
}
完全背包
题目地址:P1616 疯狂的采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
朴素处理方式:
基于01背包,在i,j
确定时,不断增加此物品的数量并向前查询,时间复杂度O(n^3).
测试代码:
@Test
void contextLoads3() {
int n = 3;
int m = 70;
int[] w = {71,69,1};
int[] v = {100,1,2};
int[] dp = new int[71];
for (int i = 0; i < 3; i++) {
for (int j =70; j >= w[i] ; j--) {
for (int k = 0; k <= m/w[i]&&(j-k*w[i]>=0); k++) {
dp[j] = Math.max(dp[j],dp[j-k*w[i]] + k*v[i]);
}
}
for (int t = 0; t < dp.length; t++) {
System.out.print(dp[t]);
}
System.out.println();
}
System.out.println("ans"+dp[70]);
}
优化:
O(n^3) 的复杂度是无法承受的,可以想象的是:如果在i,j为特值的某一点,若不拿,则dp[i][j]=dp[i-1][j]
,若拿取,则dp[i][j] = dp[i][j-w[i]] + v[i]
,所以状态转移方程为:dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])
,压缩后dp[j]=max(dp[j],dp[j-w[i]]+v[i])
,与01背包相同,但是遍历顺序从前向后(从前向后用到本层数据,从后向前用到上一层数据)
优化代码:
@Test
void contextLoads4() {
int n = 3;
int m = 70;
int[] w = {71,69,1};
int[] v = {100,1,2};
int[] dp = new int[71];
for (int i = 0; i < 3; i++) {
for (int j =w[i]; j <=70 ; j++) {
dp[j] = Math.max(dp[j],dp[j-w[i]] + v[i]);
}
for (int t = 0; t < dp.length; t++) {
System.out.print(dp[t]);
}
System.out.println();
}
System.out.println("a
问题解释:
为何01背包和完全背包的继承关系不同(dp[i][...],dp[i-1][...]
)?,也就是为什么01背包继承上层,完全背包继承本层?
实际上二者本质相同,如果01背包中存在相同元素,继承上层等同于完全背包的继承本层;二者关键区别是物品数量和遍历顺序,所以01背包对于一个元素,不存在递进关系,而完全背包本层加数量时可以递进.
多重背包
多重背包可以转化为01背包问题,对于不同数量的元素,使用二进制转化,可以使得原本s种选择转为log(s)次选择,主要变动在于处理输入数据上,而非核心算法,故不详细说明