1.0-1背包问题
针对全局问题提出的分治问题,对于每个物品只有选择或不选择两种情况,所以属于0-1背包。最终的结果是得到如下所示的决策图:
解决方案:
1.分治法
抽象问题,背包问题抽象为寻找组合(x1,x2,x3…xn,其中xi取0或1,表示第i个物品取或者不取),vi代表第i个物品的价值,wi代表第i个物品的重量,总物品数为n,背包容量为c。
- 建模,问题即求max(x1v1 + x2v2 + x3v3 + … + xnvn)。
- 约束条件,x1w1 + x2w2 + x3w3 + … + xnwn < c
- 定义函数KS(i,j):代表当前背包剩余容量为j时,前i个物品最佳组合所对应的价值;
- 递推关系式:对于第i个物品,有两种可能:
背包剩余容量不足以容纳该物品,此时背包的价值与前i-1个物品的价值是一样的,KS(i,j) = KS(i-1,j)
背包剩余容量可以装下该商品,此时需要进行判断,因为装了该商品不一定能使最终组合达到最大价值,如果不装该商品,则价值为:KS(i-1,j),如果装了该商品,则价值为KS(i-1,j-wi) + vi,从两者中选择较大的那个,
public class Solution{
int[] vs = {0,2,4,3,7};
int[] ws = {0,2,3,5,5};
@Test
public void testKnapsack1() {
int result = ks(4,10);
System.out.println(result);
}
private int ks(int i, int c){
int result = 0;
if (i == 0 || c == 0){
// 初始条件
result = 0;
} else if(ws[i] > c){
// 装不下该珠宝
result = ks(i-1, c);
} else {
// 可以装下
int tmp1 = ks(i-1, c);
int tmp2 = ks(i-1, c-ws[i]) + vs[i];
result = Math.max(tmp1, tmp2);
}
return result;
}
}
2.动态规划之自上向下记忆
主要是通过递归填表实现
public class Solution{
int[] vs = {0,2,4,3,7};
int[] ws = {0,2,3,5,5};
Integer[][] results = new Integer[5][11];
@Test
public void testKnapsack2() {
int result = ks2(4,10);
System.out.println(result);
}
private int ks2(int i, int c){
int result = 0;
// 如果该结果已经被计算,那么直接返回
if (results[i][c] != null) return results[i][c];
if (i == 0 || c == 0){
// 初始条件
result = 0;
} else if(ws[i] > c){
// 装不下该珠宝
result = ks(i-1, c);
} else {
// 可以装下
int tmp1 = ks(i-1, c);
int tmp2 = ks(i-1, c-ws[i]) + vs[i];
result = Math.max(tmp1, tmp2);
results[i][c] = result;
}
return result;
}
}
3.动态规划之自下而填表
i表示的是目前存在可使用的物品序列到i,也就是1…i都可以用;
j表示的是所剩下的容积
这样,我们就得到了最后的结果:13。根据结果,我们可以反向找出各个物品的选择,寻找的方法很简单,就是从i=4,j=10开始寻找,如果ks(i-1,j)=ks(i,j),说明第i个物品没有被选中,从ks(i-1,j)继续寻找。否则,表示第i个物品已被选中,则从ks(i-1,j-wi)开始寻找。
public class Solution{
int[] vs = {0,2,4,3,7};
int[] ws = {0,2,3,5,5};
Integer[][] results = new Integer[5][11];
@Test
public void testKnapsack3() {
int result = ks3(4,10);
System.out.println(result);
}
private int ks3(int i, int j){
// 初始化
for (int m = 0; m <= i; m++){
results[m][0] = 0;
}
for (int m = 0; m <= j; m++){
results[0][m] = 0;
}
// 开始填表
for (int m = 1; m <= i; m++){
for (int n = 1; n <= j; n++){
if (n < ws[m]){
// 装不进去
results[m][n] = results[m-1][n];
} else {
// 容量足够
if (results[m-1][n] > results[m-1][n-ws[m]] + vs[m]){
// 不装该珠宝,最优价值更大
results[m][n] = results[m-1][n];
} else {
results[m][n] = results[m-1][n-ws[m]] + vs[m];
}
}
}
}
return results[i][j];
}
}
接下来是优化:
由递推关系式可知,每次求解 KS(i,j)只与KS(i-1,m) {m:1…j} 有关,于是我们每次只关心一个i对应的所有j情况的一维数组即可,将上面的二维数组进行标记。需要注意的是,由于KS(j)是由它前面的KS(m){m:1…j}推导出来的,所以在第二轮循环扫描的时候应该由后往前进行计算,因为如果由前往后推导的话,前一次循环保存下来的值可能会被修改,从而使得后续的计算无法使用对应位置的原值。
借鉴链接:https://www.cnblogs.com/mfrank/p/10587463.html
public class Solution{
int[] vs = {0,2,4,3,7};
int[] ws = {0,2,3,5,5};
int[] newResults = new int[11];
@Test
public void test() {
int result = ksp(4,10);
System.out.println(result);
}
private int ksp(int i, int c){
// 开始填表
for (int m = 0; m < vs.length; m++){
int w = ws[m];
int v = vs[m];
for (int n = c; n >= w; n--){
newResults[n] = Math.max(newResults[n] , newResults[n - w] + v);
}
// 可以在这里输出中间结果
System.out.println(JSON.toJSONString(newResults));
}
return newResults[newResults.length - 1];
}
}
2.完全背包问题
有N种物品和一个容量为T的背包,每种物品都就可以选择任意多个,第i种物品的价值为P[i],体积为V[i],求解:选哪些物品放入背包,可卡因使得这些物品的价值最大,并且体积总和不超过背包容量。
跟01背包一样,完全背包也是一个很经典的动态规划问题,不同的地方在于01背包问题中,每件物品最多选择一件,而在完全背包问题中,只要背包装得下,每件物品可以选择任意多件。从每件物品的角度来说,与之相关的策略已经不再是选或者不选了,而是有取0件、取1件、取2件…直到取⌊T/Vi⌋(向下取整)件。
1.自上而下记忆法
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)
public static class Knapsack {
private static int[] P={0,5,8};
private static int[] V={0,5,7};
private static int T = 10;
private Integer[][] results = new Integer[P.length + 1][T + 1];
@Test
public void solve2() {
int result = ks2(P.length - 1,10);
System.out.println("最大价值为:" + result);
}
private int ks2(int i, int t){
// 如果该结果已经被计算,那么直接返回
if (results[i][t] != null) return results[i][t];
int result = 0;
if (i == 0 || t == 0){
// 初始条件
result = 0;
} else if(V[i] > t){
// 装不下该珠宝
result = ks2(i-1, t);
} else {
// 可以装下
// 取k个物品,取其中使得价值最大的
for (int k = 0; k * V[i] <= t; k++){
int tmp2 = ks2(i-1, t - V[i] * k) + P[i] * k;
if (tmp2 > result){
result = tmp2;
}
}
}
results[i][t] = result;
return result;
}
}
2.自下而上填表法
public static class Knapsack {
private static int[] P={5,8};
private static int[] V={5,7};
private static int T = 10;
private int[][] dp = new int[P.length + 1][T + 1];
@Test
public void solve3() {
for (int i = 0; i < P.length; i++){
for (int j = 0; j <= T; j++){
for (int k = 0; k * V[i] <= j; k++){
dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
}
}
}
System.out.println("最大价值为:" + dp[P.length][T]);
}
}
优化后:
public static class Knapsack {
private static int[] P={0,5,8};
private static int[] V={0,5,7};
private static int T = 10;
private int[] newResults = new int[T + 1];
@Test
public void resolve4() {
int result = ksp(P.length,T);
System.out.println(result);
}
private int ksp(int i, int t){
// 开始填表
for (int m = 0; m < i; m++){
for (int n = V[m]; n <= t; n++){
//这里的意思是如果取n<V[m],也就意味着目前容积是不足以放下m位置对应的物品的,数组的值还是会和上面一样的,所以不取
//通过下面这个语句在更新数组的具体值
newResults[n] = Math.max(newResults[n] , newResults[n - V[m]] + P[m]);
}
// 可以在这里输出中间结果
System.out.println(JSON.toJSONString(newResults));
}
return newResults[newResults.length - 1];
}
}