动态规划
- 动态规划 Dynamic Programming,核心思想就是将大问题划分成小问题进行解决,从而一步一步的获得最优解的处理算法
- 动态规划跟分治算法思想类似,但动态规划算法会依赖到上一次计算的结果,每次求解是建立在上一次子阶段的结果基础之上进一步处理,而分治算法分解出来问题往往是独立的
- 动态规划一般可以通过填表的方式进行逐步推进得到最优解
0/1背包问题
01背包问题是经典的利用动态规划算法求解的一个经典案例
有n个物品,他们有各自的重量(w)和价值§,有一个给定容量capacity容量的背包,每件物品只能拿1次,如何将让背包装入的物品具有最大价值
假定 有3个商品 背包的容量是4
物品 | 重量w | 价值p |
---|---|---|
手机 | 1 | 1500 |
电脑 | 4 | 3000 |
电视 | 3 | 2000 |
利用动态规划填表的思想得下一下的表格
- 假定 i 为拿第几件商品表示为列 j为背包的容量表示为行
- 为了方便计算统一,添加一个0件物品和0容量背包的边界情况
- 最优解为B(i,j) 例如若B(0,0)表示拿第0件物品 背包容量为0的时候的最优解 我们很容易得到是0
- B(0,1) 当拿0件物品背包容量是0的时候价值也是0因为没有拿任何物品
- B(1,0) 当拿1件物品背包容量为0的时候因为背包容量为0不能装下任何商品则B(1,0)=0
- B(1,1) 当拿第1件物品且背包容量为1的时候,发现手机的重量正好是1满足背包的容量则B(1,1)=1500,这里1500就是第一件物品即手机的价值 同理B(1,2…4)因为只拿了一件物品无论背包容量多大价值都是1500
- B(2,1) 当拿第2件物品的时候,这时发现物品2电脑的重量是4无法装入容量为1的背包,放不到背包,此时只能去B(2-1,1)的结果,即是第二件物品拿不下,即用装下上一件物品的最优解,同理B(2,2)和B(2,3)
- B(2,4) 这里情况发现了遍历,当前背包的容量是4,放下是可以装下物品2的,此时就有两种情况
- 不拿当前商品 即 B(1,4)即只拿了上一件商品
- 拿当前商品 j价值就是B(1,4-w[i])+p[i] 这里的4-w[i]是因为本来他的容量是4,然后因为你拿了当前商品背包的容量就变少了成了4-当前商品的重量w[i],得到的结果就是上一件商品在重量为4-w[i]时的最优解,然后加上因为此时已经拿了当前商品需要加上当前商品的价值p[i]
- 换成当前我们这里的列子就是B(1,4)与B(1,4-4) + 3000进行比较大小,大的即是B(2,4)的最优解
- 在看一个特殊的列子B(3,4)
- 不拿商品的价值 为B(2,4)为 3000
- 拿了商品则是B(2,4-3)+p[4] ==> B(2,1)=1500 + 2000 = 3500
- 此时发现规律,如果发现当前商品背包放不下的情况即 j < w[i]时,此时的B(i,j)只能等于只拿上一件物品的最优解 即B(i-1,j) 记为 if j<w[i] ==> B(i,j)=B(i-1,j)
- 如果发现当前商品可以装下,就分为装和不装两种情况
- 不装最优解跟我们发现商品装不下的结果是一样的 即 B(i,j)=B(i-1,j)
- 装的情况是我们上一件商品在容量减去当前商品重量的最优解加上当前商品的价值 即 **B(i,j)=B(i-1,j-w[i])+p[i] **
- 此时这两种情况的大的那一个为最优解 B(i,j)=max(B(i-1,j),B(i-1,j-w[i])+p[i])
i/j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1500 | 1500 | 1500 | 1500 |
2 | 0 | 1500 | 1500 | 1500 | 3000 |
3 | 0 | 1500 | 1500 | 3000 | 3500 |
代码示例
package com.corn.algorithm.dynamicprogram;
import java.util.Arrays;
// 0/1 背包问题 动态规划算法
public class KnapsackProblem {
public static void main(String[] args) {
// knapsackSolution1();
knapsackSolution2();
}
/**
* 使用动态数组 优化空间复杂度
* 其实每次更新二维数组的时候
* 需要注意的是二层循环遍历列的时候 需要从右往左遍历 因为右边的值一定是依赖左边的值
*/
public static void knapsackSolution2() {
// 价格
int[] prices = {0, 1500, 3000, 2000};
// 每件物品的重量
int[] weight = {0, 1, 4, 3};
// 背包容量
int capacity = 4;
int[] table = new int[capacity + 1];
for (int i = 0; i < prices.length; i++) {
for (int j = capacity; j > 0; j--) {
// 只处理放得下的情况,如果放不下 说明当前这列的值与上一行一样可以不变化
if (weight[i] <= j) {
// table[j] = Math.max(table[j], table[j - weight[i]] + prices[i]);
if (table[j] > table[j - weight[i]] + prices[i]) {
// 如果不放商品的价值高
table[j] = table[j];
} else {
// 加入商品的价值高
table[j] = table[j - weight[i]] + prices[i];
}
}
}
System.out.println(Arrays.toString(table));
}
}
private static void knapsackSolution1() {
// 价格
int[] prices = {0, 1500, 3000, 2000};
// 每件物品的重量
int[] weight = {0, 1, 4, 3};
// 背包容量
int capacity = 4;
int[][] table = new int[weight.length][capacity + 1];
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j < capacity + 1; j++) {
if (weight[i] > j) {
// 当前放入商品的重量 超过了 背包的容量此时 最大值只能是 B(i-1,j); 意思就是上一个商品装入的最高价值
table[i][j] = table[i - 1][j];
} else {
// 如果可以装入则判断 是装入之后的价格高 还是不装只收的价值搞
// 公式为 max(B(i-1,j),B(i-1,j-w[i])+p[i])
//B(i-1,j) 为装入上一件商品的最大价值
// B(i-1,j-w[i])+p[i] j-w[i]为背包容量少去当前商品重量等于背包装入商品之后的剩余容量 此时得到值是上一件物品在背包容量为j-w[i]时的最大值 p[i] 为当前商品的价值 连起来就是 B(i-1,j-w[i]) 上一件商品在除去当前装入商品之后重量的最大价值 + 当前商品的价值
table[i][j] = Math.max(table[i - 1][j], table[i - 1][j - weight[i]] + prices[i]);
}
}
}
for (int[] ints : table) {
System.out.println(Arrays.toString(ints));
}
int[] path = new int[weight.length];
// 处理路径 遍历最后一列即可 从下往上遍历
for (int i = weight.length - 1; i >= 1; i--) {
// 如果拿和不拿最优解相同则 就不拿标记上0
if (table[i][capacity] == table[i - 1][capacity]) {
path[i] = 0;
} else {
// 拿了的价值大于 不拿的价值 其实说明要拿标记上1 最后需要注意的是要剪去capacity中已经拿了的部分
path[i] = 1;
capacity -= weight[i];
}
}
// path[1] = (table[1][capacity] > 0) ? 1 : 0; ///上面循环中没有判断1,只要>0就可以证明拿了
System.out.println(Arrays.toString(path));
}
}