基本思想
动态规划算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获得最优解;这与分治算法相似,但是,动态规划求解的问题,经过分解得到的子问题不是互相独立的,(即:下一个子阶段的求解是建立在上一个子阶段的解的基础上,然后进行进一步求解)。
动态规划问题可以通过填表的方式来逐步推进,得到最优解。
动态规划——背包问题
问题介绍
背包问题分为01背包和完全背包,01背包指每个物品不能重复放入,完全背包指每个物品可以无条件用,完全背包可以转化为01背包。下面分析01背包问题。
有一个背包,容量为4磅,有如下物品:
要求:
1)装入背包的物品的总价值最大,并且重量不超出背包容量;
2)装入物品不能重复
分析思路
先说明一些变量:
i 第i个物品
w 重量,w[i] 第i个物品的重量
v(val) 价值,v[i](val[i]) 第i个物品的价值
有n个物品
背包容量为m
在将物品放入背包时,根据w[i]和v[i] 即重量和价值来确定是否放入背包。
j 背包容量,可变,(下面逐次递增,1->2->3->4, j <= m),为动态规划做准备
v[i][j] 在前 i 个物品中,能够装入容量为 j 的背包中 的最大价值。
算法过程
(1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是 0,为了保证商品和容量从1开始。
(2) 当 w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当 j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} ;// 当 准备加入的新增的商品的容量小于等于当前背包的容量时,策略为:取 上一个单元格的最大价值 和 新增商品价值+剩余容量装入上一个单元格商品价值的总价值 里取最大的;
分析即:
// 装入的方式: v[i-1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值
v[i-1][j-w[i]] : 装入 i-1 商品,到剩余空间 j-w[i]的最大值
因此,当 j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} 。
图解分析及验证
下面给出图解分析,并对算法过程进行验证。
注意,通过上述算法流程,只能得到最大价值的值,还需要记录最大价值情况下的策略,即:背包容量为4磅时,里面具体装入什么商品。分析如下:
设置一个记录矩阵path, i代表商品的编号,在记录时,将最优情况下的path[i][j]记录为1,最后的放置情况才是最终的最优策略,因此要逆向遍历path矩阵,遍历时容量要动态变化,商品标号也要递减,从而寻找下一个放入背包的商品标号。
记录放入商品的代码: //为了记录放入商品的情况,定义一个二维数组 int[][] path = new int[n + 1][m + 1];//为了记录商品存放到背包里的情况,需要对上面的语句进行修改 if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) { v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]]; //把当前情况记录到path path[i][j] = 1; } else { v[i][j] = v[i - 1][j]; }System.out.println("========商品放置情况============"); int i = path.length - 1;//行下标最大值 int j = path[0].length - 1;//列下标最大值 while (i > 0 && j > 0) {//从path最后开始,逆序遍历 找 if (path[i][j] == 1) { System.out.printf("第%d个商品放入背包\n", i); j -= w[i-1];//j的容量要调整 } i--; }path矩阵运行后的结果:
代码实现
package com.study.dynamic;
/**
* @author 小
* @version 1.0
*/
public class KnapsackProblem {
public static void main(String[] args) {
int[] w = {1, 4, 3};//保存物品重量
int[] val = {1500, 300, 2000};//物品价值,公式里的v[i]
int m = 4;//背包容量
int n = val.length;//物品数量
//为了记录放入商品的情况,定义一个二维数组
int[][] path = new int[n + 1][m + 1];
//创建二维数组
//v[i][j]:表示 在前i个物品中,能够装入容量为j的背包中的 最大价值
int[][] v = new int[n + 1][m + 1];
//初始化第一行和第一列 0其实是默认的,也可以不设置,就是0
for (int i = 0; i < v.length; i++) {
v[i][0] = 0;//将第一列设为0
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0;//将第一行设为0
}
//根据动态规划的公式,进行动态规划处理
for (int i = 1; i < v.length; i++) {//不处理第一行
for (int j = 1; j < v[0].length; j++) {
//公式 w[i]>j时,v[i][j]=v[i-1][j]
if (w[i - 1] > j) {//因为程序的i从1开始,公式的i从0开始,因此要-1
v[i][j] = v[i - 1][j];
} else {//公式 w[j]<=j时,v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
//因为i在程序里从1开始,因此 val[]、w[]的i要减掉1
// v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
//为了记录商品存放到背包里的情况,需要对上面的语句进行修改
if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
//把当前情况记录到path
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
}
//输出看一下v
System.out.println("======动态规划结果======");
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();//换行
}
//输出放入的哪些商品
//遍历path,下面方法遍历会把所有放入情况都得到,实际上我们只需最后的放入
// System.out.println("========商品放置情况============");
// for (int i = 0; i < path.length; i++) {
// for (int j = 0; j < path[i].length; j++) {
// if (path[i][j] == 1) {
// System.out.printf("第%d个商品放入背包\n", i);
// }
// }
// }
//
System.out.println("========商品放置情况============");
int i = path.length - 1;//行下标最大值
int j = path[0].length - 1;//列下标最大值
while (i > 0 && j > 0) {//从path最后开始,逆序遍历 找
if (path[i][j] == 1) {
System.out.printf("第%d个商品放入背包\n", i);
j -= w[i-1];//j的容量要调整
}
i--;
}
}
}
运行结果:
可以看到,结果与前述分析一致。