0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
分析一波,面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。
解决办法:声明一个 大小为 m[n][c] 的二维数组,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)
究竟是拿还是不拿,自然是比较这两种情况那种价值最大。
由此可以得到状态转移方程:
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];
例:0-1背包问题。在使用动态规划算法求解0-1背包问题时,使用二维数组m[i][j]存储背包剩余容量为j,可选物品为i、i+1、……、n时0-1背包问题的最优值。绘制
价值数组v = {8, 10, 6, 3, 7, 2},
重量数组w = {4, 6, 2, 2, 5, 1},
背包容量C = 12时对应的m[i][j]数组。
(第一行和第一列为序号,其数值为0)
如m[2][6],在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int count = scanner.nextInt(); int capacity = scanner.nextInt(); int[] values = IntStream.range(0, count).map(num -> scanner.nextInt()).toArray(); int[] weights = IntStream.range(0, count).map(num -> scanner.nextInt()).toArray(); //0-1背包标准解答,求解二维数组; solve01BagGeneral(count,capacity,values,weights); //0-1背包优化解答,求解一维数组; solve01BagOptimization(count,capacity,values,weights); } private static void solve01BagGeneral(int count, int capacity, int[] values, int[] weights) { int[][] plans = new int[count+1][capacity+1]; for(int i=1; i<=count; i++ ) { for(int j=1; j<=capacity; j++) { if (j >= weights[i-1]) { plans[i][j] = Math.max(plans[i-1][j], plans[i-1][j-weights[i-1]] + values[i-1] ); }else { plans[i][j] = plans[i-1][j]; } } } Stream.of(plans).forEach(t -> System.out.println(Arrays.toString(t))); int[] x = new int[count]; int c=capacity; for(int i=count; i>0; i--) { if (plans[i][c] == plans[i-1][c]) { x[i-1] = 0; }else { x[i-1] = 1; c -= weights[i-1]; } } System.out.println(Arrays.toString(x)); }
到这一步,可以确定的是可能获得的最大价值,但是我们并不清楚具体选择哪几样物品能获得最大价值。
另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。
m[n][c]为最优值,如果m[n][c]=m[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][c]继续构造最优解;当x[n]=1时,则由x[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。(这段全抄算法书,实在不知道咋解释啊。。)
int[] x = new int[count]; int c=capacity; for(int i=count; i>0; i--) { if (plans[i][c] == plans[i-1][c]) { x[i-1] = 0; }else { x[i-1] = 1; c -= weights[i-1]; } }
二、优化空间复杂度
先考虑一下上面的状态转移方程如何实现,肯定有一个主循环i = 1...N,每次算出来二维数组dp[i][0..V]的所有值。那么如果只用一个数组f[0...V],能不能保证第i次循环结束后f[v]就是我们定义的状态f[i][v]呢?
f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?
事实上,这要求在每次主循环中我们以v=V...0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:
for i in 0 ... N
for v = V ... 0
f[v] = max{f[v], f[v-c[i]] + w[i]}
private static void solve01BagOptimization(int count, int capacity, int[] values, int[] weights) { int[] dp = new int[capacity+1]; for(int i=1; i<=count; i++ ) { for(int j=capacity; j>=weights[i-1]; j--) { dp[j]=Math.max(dp[j],dp[j-weights[i-1]]+values[i-1]); } } System.out.println(Arrays.toString(dp)); }
三、完全背包问题
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价格是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大
思路
这个问题类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略 已非取或不取两种,而且右取0件、取1件、取2件...等很多种。如果仍然按照01背包的思路,令dp[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:
dp[i][v] = max{dp[i-1][v - k * c[i]] + k * w[i] | 0 <= k * c[i]<= v}
private static void solveFullBagGeneral(int count, int capacity, int[] values, int[] weights) { int[][] plans = new int[count+1][capacity+1]; for(int i=1; i<=count; i++ ) { for(int j=1; j<=capacity; j++) { int max=0; for( int k=0; (k * weights[i-1])<=capacity; k++ ){ if (j >= k* weights[i-1]) { max = Math.max(max, plans[i - 1][j - k * weights[i - 1]] + k * values[i - 1]); } } plans[i][j]=max; } } Stream.of(plans).forEach(t -> System.out.println(Arrays.toString(t))); }
四、完全背包优化解;
转化为01背包求解
最简单的想法是:考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转换为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。但是这样完全没有改进时间复杂度,但这毕竟给了我们将完全背包转换为01背包问题的思路:将一种物品拆成多件物品
O(VN)的算法
这个算法使用一维数组,先看伪代码:
for i = 1 ... N
for v = 0 ... V
f[v] = max{f[v], f[v-cost] + weight}
你会发现,这个伪代码与01背包的伪代码只有v的循环次序不同而已。为什么这样一改就行呢?首先,想想为什么01背包问题中要按照v=V...0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰好是每种物品可选无限件,所以在考虑“加选一件dii种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][c-v[i]],所以就可以并且必须采用v=0...V的顺序循环
原文链接:https://blog.csdn.net/wzy_1988/article/details/12260343
https://www.cnblogs.com/zyacmer/p/9961710.html