- 问题描述:给定n个物品和一个容量为C的背包,请给出物品装入背包的方案,使得背包中物品的总价值M最大,并满足:
- 每个物品I的重量为Wi,价值为Vi。
- 每个物品不可拆分,要么完整装入背包,要么不在背包里。
- 背包中物品的总重量不能超过容量C。
分析一波,面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。
本问题是满足动态规划的,什么是动态规划?
- 动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
- 动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
可以应用动态规划求解的问题主要由四个特点:
- 问题是求最优解
- 整体问题的最优解依赖于各个子问题的最优解
- 大问题分解成若干小问题,这些小问题之间还有相互重叠的更小的子问题
- 从上往下分析问题,从下往上求解问题
(即是如果一个问题可以使用分治法来解决的,但是其子问题无法相互独,会出现重复计算,坚持使用分治会导致增加计算量。这时可以采取动态规划算法来解决问题)。
以下是两种不同的动态规划解决方案:
- 自上而下:你从最顶端开始不断地分解问题,直到你看到问题已经分解到最小并已得到解决,之后只用返回保存的答案即可。这叫做记忆存储(Memoization)。
- 自下而上:你可以直接开始解决较小的子问题,从而获得最好的解决方案。在此过程中,你需要保证在解决问题之前先解决子问题。这可以称为表格填充算法(Tabulation,table-filling algorithm)。
例子详解
算法分析中动态规划的四个基本步骤
1、描述优解的结构特征。
2、递归地定义一个最优解的值。
3、自底向上计算一个最优解的值。
4、从已计算的信息中构造一个最优解。
解题思路:针对动态规划普遍采用建表的形势,通过观察建立好一张合适的表格用来记录子问题的最优解。
本题要求是最大的价值,而限制价值的是背包容量。
可以把背包容量划分为从1 到 c,随着容量的增加来不断扩大价值,而且物品的数量也是从少到多的选择,所以可以申明一个数组arr[arr_IDrow][arr_Wcolumn],则m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值。
那么我们可以很容易分析得出 m[i][j] 的计算方法,
(1). j < w[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿 m[ i ][ j ] = m[ i-1 ][ j ]
(2). j>=w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。
- 如果拿取,m[ i ][ j ]=m[ i-1 ][ j-w[ i ] ] + v[ i ]。 这里的m[ i-1 ][ j-w[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。
- 如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)
究竟是拿还是不拿,自然是比较这两种情况那种价值最大。
可以得到下表(为了方便表示递推式,多扩充了一行和一列,保证了递推式的通用)
如m[3][4],在面对第3件物品,背包容量为4时我们可以选择不拿,那么获得价值仅为第1件和第2件物品的价值22,如果拿,就要把第1件物品拿出来,放第3件物品,价值30,那我们当然是选择拿。m[3][4]=m[2][4-3]+20=10+20=30(4-3是当前容量减去放入3号物品的重量后包内剩下的容量);依次类推,得到m[4][5]就是考虑所有物品,背包容量为C时的最大价值.
由图可以得到状态转移方程:
if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
m[i][j]=m[i-1][j];
主要的算法:
//声明的变量
private int[][] arr;
private int arr_IDrow; //行 放id号
private int arr_Wcolumn; //列 放背包重量
private int flag[]; //是否装入,1为装入,0为不装。flag[1] = 1 表示id为1的物品装入
private int M;//总价值
private Map<Integer,Integer> idToV = new HashMap<>(); //id映射物品的价值
private Map<Integer, Integer> idToW = new HashMap<>(); //id映射物品的重量
//动态规划解决问题
public void dpFun() {
arr = new int[arr_IDrow+1][arr_Wcolumn+1];
flag = new int[arr_IDrow+1];
//开始判断,并且填表,填入商品的价格
for(int row=1;row<=arr_IDrow;row++) {
for(int col=1;col<=arr_Wcolumn;col++) {
if(col>=idToW.get(new Integer(row))) {
arr[row][col] = Math.max(arr[row-1][col],arr[row-1][col-idToW.get(row)]+idToV.get(row)) ;
}else {
arr[row][col]=arr[row-1][col];
}
}
}
M = arr[arr_IDrow][arr_Wcolumn];//最大容量
traceback();//回溯判断是否拿取
}
另起一个 flag[ ] 数组,flag[i]=0表示不拿,flag[i]=1表示拿。
m[n][c]为最优值,如果m[n][c]=m[n-1][c] ,说明有没有第n件物品都一样,则flag[n]=0 ; 否则 flag[n]=1。当flag[n]=0时,由flag[n-1][c]继续构造最优解;当flag[n]=1时,则由flag[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。
//回溯判断是否拿取
public void traceback()
{
for (int i = arr_IDrow; i > 1; i--)
{
if (arr[i][arr_Wcolumn] == arr[i - 1][arr_Wcolumn])
flag[i] = 0;
else
{
flag[i] = 1;
arr_Wcolumn -= idToW.get(i);
}
}
//判断第一行id:1
flag[1] = (arr[1][arr_Wcolumn] > 0) ? 1 : 0;
}
完整的代码:
public class pack0_1 {
private int[][] arr;
private int arr_IDrow; //行 放id号
private int arr_Wcolumn; //列 放背包重量
private int flag[]; //是否装入,1为装入,0为不装。flag[1] = 1 表示id为1的物品装入
private int M;//总价值
private Map<Integer,Integer> idToV = new HashMap<>(); //id映射物品的价值
private Map<Integer, Integer> idToW = new HashMap<>(); //id映射物品的重量
public pack0_1() {
// TODO 自动生成的构造函数存根
initArr();
dpFun();
outPlan();
}
//动态规划解决问题
public void dpFun() {
arr = new int[arr_IDrow+1][arr_Wcolumn+1];
flag = new int[arr_IDrow+1];
//开始判断,并且填表,填入商品的价格
for(int row=1;row<=arr_IDrow;row++) {
for(int col=1;col<=arr_Wcolumn;col++) {
if(col>=idToW.get(new Integer(row))) {
arr[row][col] = Math.max(arr[row-1][col],arr[row-1][col-idToW.get(row)]+idToV.get(row)) ;
}else {
arr[row][col]=arr[row-1][col];
}
}
}
M = arr[arr_IDrow][arr_Wcolumn];
traceback();
}
//读取文件初始化数组
private void initArr() {
File inFile = new File("C:\\Users\\HP\\Desktop\\input.txt");
InputStreamReader input = null;
BufferedReader bf = null;
try {
input = new FileReader(inFile);
bf = new BufferedReader(input);
//按行读取文件
String str = bf.readLine();
arr_Wcolumn = Integer.valueOf(str);
while((str=bf.readLine())!=null) {
String[] strArr = new String[] {};
strArr = str.split("\t");//按tab来切割字符串
int i = 1;
idToW.put(Integer.valueOf(strArr[0]), Integer.valueOf(strArr[i]));
idToV.put(Integer.valueOf(strArr[0]), Integer.valueOf(strArr[i+1]));
}
arr_IDrow = idToV.size();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
try {
if(input!=null||bf!=null) {
input.close();
bf.close();
}
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//输出结果文件
private void outPlan() {
File outFile = new File("C:\\Users\\HP\\Desktop\\output.txt");
OutputStreamWriter output = null;
try {
output = new FileWriter(outFile);
output.write(M+"\r\n");
for (int i = 1; i < arr.length; i++) {
output.write(""+i+"\t"+flag[i]+"\t"+idToV.get(i)+"\r\n");
}
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
try {
if(output!=null) {
output.close();
}
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//回溯判断是否拿取
public void traceback()
{
for (int i = arr_IDrow; i > 1; i--)
{
if (arr[i][arr_Wcolumn] == arr[i - 1][arr_Wcolumn])
flag[i] = 0;
else
{
flag[i] = 1;
arr_Wcolumn -= idToW.get(i);
}
}
//判断第一行id:1
flag[1] = (arr[1][arr_Wcolumn] > 0) ? 1 : 0;
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
new pack0_1();
}
}
结果: