目录
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回
-1
。示例 1:
输入: coins = [1, 2, 5], amount = 11 输出: 3 解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3 输出: -1
动态规划求解,类似0-1背包问题。
一、0-1背包问题
1.1 问题描述
有编号分别为a,b,c,d的四件物品,它们的重量分别是2,3,4,5,它们的价值分别是3,4,5,6,每件物品数量只有一个,现在给你个承重为8的背包,如何让背包里装入的物品具有最大的价值总和?
1 | 2 | 3 | 4 | |
---|---|---|---|---|
w(体积) | 2 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 5 | 6 |
1.2 动态规划过程
a) 把背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选),Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积(重量);
b) 建立模型,即求max(V1X1+V2X2+…+VnXn);
c) 约束条件,W1X1+W2X2+…+WnXn<capacity;
d) 定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值;
e) 最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。判断该问题是否满足最优性原理,采用反证法证明:
假设(X1,X2,…,Xn)是01背包问题的最优解,则有(X2,X3,…,Xn)是其子问题的最优解,
假设(Y2,Y3,…,Yn)是上述问题的子问题最优解,则理应有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V2X2+V3X3+…+VnXn)+V1X1;
而(V2X2+V3X3+…+VnXn)+V1X1=(V1X1+V2X2+…+VnXn),则有(V2Y2+V3Y3+…+VnYn)+V1X1 > (V1X1+V2X2+…+VnXn);
该式子说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优性原理;
1.3 状态转移方程
情况一,包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
情况二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i)但价值增加了v(i);
由此可以得出递推关系式:
为什么是前一行?
因为前一行是到达当前重量时背包内物品的最大价值,如果选择不放当前物品,说明背包容量不够,那么现在的最大价值只能是前一行的值。同理,选择放当前物品,那么也应该在前一行的基础上加上当前物品的价值。
1.4 填表
初始化边界:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||||||
2 | 0 | ||||||||
3 | 0 | ||||||||
4 | 0 |
根据状态转移方程填写上表:
1.5 代码
private int maxValue(int[] w, int[] v, int capacity) {
int row = w.length + 1;
int col = capacity + 1;
int[][] result = new int[row][col];
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (j < w[i - 1]){
result[i][j] = result[i-1][j];
}else {
result[i][j] = Math.max(result[i - 1][j-w[i-1]] + v[i-1],result[i-1][j]);
}
}
}
return result[row-1][col-1];
}
1.6 回溯法求解
最优解保存在result[row-1][col-1]中,解决方式:
1) V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
2) V(i,j)=V(i-1,j-w(i))+v(i)实时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
3) 一直遍历到i=1结束为止,所有解的组成都会找到。
代码:
参数说明:
result:结果数组
goods:用来保存物品选择结果的数组
w:物品重量数组
v:物品价值数组
i,j:最优解的坐标
private static void getGoods(int[][] result, int[] goods,int[] w,int[] v,int i,int j) {
if (i >= 1){
if (result[i][j] == result[i-1][j]){
goods[i] = 0;
getGoods(result,goods,w,v,i-1,j);
}else if (j - w[i - 1] >= 0 && result[i][j] == result[i-1][j-w[i - 1]] + v[i - 1]){
goods[i] = 1;
getGoods(result,goods,w,v,i - 1,j - w[i - 1]);
}
}
}
1.7 优化
用一维数组来保存结果,相当于保存每一列的最大值即可,但是需要注意的是里面第二层循环要倒置。考虑一下【2,8】这一项
它是由result【1,6】+ v【2】得到的,所以计算当前容量对应的价值时,需要上一行的状态,因此从后向前计算就可解决这一问题,如果从前往后计算的话,那么上一行的状态就被覆盖了。
private int maxValue2(int[] w, int[] v, int capacity) {
//优化
int length = capacity + 1;
int[] result = new int[length];
for (int j = 0; j < w.length; j++){
for (int i = length - 1; i >= 1; i--) {
if (w[j] <= i){
result[i] = Math.max(result[i],result[i - w[j]]+v[j]);
}
}
}
return result[length-1];
}
二、完全背包问题
2.1 问题描述
有编号分别为a,b,c,d的四件物品,它们的重量分别是2,3,4,7,它们的价值分别是1,3,5,9,每件物品数量无限个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?
2.2 分析
完全背包问题是指每种物品都是无限件,其它与0-1背包问题一样。需要注意的地方就是在当前物品准备放入的时候有所不同。0-1背包问题中,当准备放入当前物品i时,那么需要比较的是不放人i时的价值result[i-1][j]和放入i时的价值result[i-1][j - w[i-1]]+v[i]。但是在完全背包问题中,由于物品的数量不限制,所以在放入当前物品i时,还要考虑可能需要继续放入当前物品。所以可以得到以下状态转移方程:
2.3 填表
初始化边界:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||||||||
2 | 0 | ||||||||||
3 | 0 | ||||||||||
4 | 0 |
根据状态转移方程填表:
2.4 代码
private int maxValue(int[] w, int[] v, int capacity) {
int row = w.length + 1;
int col = capacity + 1;
int[][] result = new int[row][col];
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (j < w[i - 1]){
result[i][j] = result[i-1][j];
}else {
result[i][j] = Math.max(result[i][j-w[i-1]] + v[i-1],result[i-1][j]);
}
}
}
return result[row-1][col-1];
}
2.5 回溯法求解
最优解保存在result[row-1][col-1]中,解决方式:
1)当 时,说明没有选择第i 个商品,则回到;
2) 当时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到;
3) 一直遍历到i=1结束为止,所有解的组成都会找到。
代码:
注意是另goods[i-1] = 0,因为当满足时,回溯到的是当前物品i,说明i-1没有放进来。
private void getGoods(int[][] result, int[] goods,int[] w,int[] v,int i,int j) {
if (i >= 1){
if (result[i][j] == result[i-1][j]){
goods[i - 1] = 0;
getGoods(result,goods,w,v,i-1,j);
}else if (j - w[i - 1] >= 0 && result[i][j] == result[i][j-w[i - 1]] + v[i - 1]){
goods[i] = 1;
getGoods(result,goods,w,v,i,j - w[i - 1]);
}
}
}
参数说明:
result:结果数组
goods:用来保存物品选择结果的数组
w:物品重量数组
v:物品价值数组
i,j:最优解的坐标
2.6 优化
空间优化,用一维数组解决二维数组的思路。仔细分析填表的过程就可以发现,当判断物品i时,只需保存小于等于物品i-1的重量的状态,而其它的状态不用保存,相当于每一列都保存最大值即可。
private static int maxValue2(int[] w, int[] v, int capacity) {
//优化
int length = capacity + 1;
int[] result = new int[length];
for (int i = 1; i < length; i++) {
for (int j = 0; j < w.length; j++){
if (i >= w[j]){
result[i] = Math.max(result[i],result[i - w[j]] + v[j]);
}
}
}
return result[length-1];
}
三、多重背包
3.1 问题描述
有编号分别为a,b,c的三件物品,它们的重量分别是1,2,2,它们的价值分别是6,10,20,他们的数目分别是10,5,2,现在给你个承重为 8 的背包,如何让背包里装入的物品具有最大的价值总和?
1 | 2 | 3 | |
---|---|---|---|
w(重量) | 1 | 2 | 3 |
v(价值) | 6 | 10 | 20 |
num(数量) | 10 | 5 | 2 |
多重背包和01背包、完全背包的区别:多重背包中每个物品的个数都是给定的,可能不是一个,绝对不是无限个。
3.2 分析
在0-1背包的基础上进行修改,此时在放入物品的时候就要考虑放入几个才能使价值最大,肯定是越多越好,但是又有数量的限制,所以要首先确定放入的数量。假设当前重量为j,准备要放入物品i,放入的数量为。
状态转移方程为:
3.3 填表
初始化表:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | ||||||||
2 | 0 | ||||||||
3 | 0 |
根据状态转移方程填表:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 |
2 | 0 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 |
3 | 0 | 6 | 12 | 20 | 26 | 32 | 40 | 46 | 52 |
3.4 代码
private int maxValue(int[] w, int[] v, int[] nums, int capacity) {
int row = w.length + 1;
int col = capacity + 1;
int[][] result = new int[row][col];
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (j < w[i - 1]){
result[i][j] = result[i-1][j];
}else {
//准备放k件物品
int count = Math.min(nums[i - 1], j/w[i - 1]);
result[i][j] = Math.max(result[i - 1][j-count * w[i-1]] + count * v[i-1],result[i-1][j]);
}
}
}
return result[row - 1][col - 1];
}
3.5 回溯法求解
原理0-1背包问题,只不过加了数量的限制。
private void getGoods(int[][] result, int[] goods,int[] w,int[] v,int[] nums,int i,int j) {
if (i >= 1) {
if (result[i][j] == result[i - 1][j]) {
goods[i] = 0;
getGoods(result, goods, w, v, nums, i - 1, j);
} else {
int count = Math.min(nums[i - 1], j / w[i - 1]);
if (j - count * w[i - 1] >= 0 && result[i][j] == result[i - 1][j - count * w[i - 1]] + count * v[i - 1]) {
goods[i] = count;
getGoods(result, goods, w, v, nums, i - 1, j - count * w[i - 1]);
}
}
}
}
3.6 优化
private int maxValue2(int[] w, int[] v, int[] nums, int capacity) {
//优化
int length = capacity + 1;
int[] result = new int[length];
for (int j = 0; j < w.length; j++){
for (int i = length - 1; i >= 1; i--) {
if (w[j] <= i){
int count = Math.min(nums[j],i / w[j]);
result[i] = Math.max(result[i],result[i - w[j]*count]+v[j]*count);
}
}
}
return result[length-1];
}
四、硬币兑换兑换
4.1 分析
根据示例1进行讲解
思路:和完全背包问题类似。只不过将物品的价值改为1,最大改为最小即可。
状态转移方程:
- 如果j - coins[i-1] >= 0成立,那么就要考虑是否使用当前硬币
result[i][j] = Math.min(result[i-1][j], 1 + result[i][j - coins[i-1]]); result[i-1][j]表示不用当前硬币,使用前一个硬币 1 + result[i][j - coins[i-1]]表示可以用当前硬币
- 如果不成立,那么就肯定不使用当前硬币
result[i][j] = result[i-1][j];
初始化result数组:
根据状态转移方程得到下表:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | MAX | MAX | MAX | MAX | MAX | MAX | MAX | MAX | MAX | MAX | MAX |
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
2 | 0 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 |
5 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
代码:
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0){
return 0;
}
final int row = coins.length + 1;
final int col = amount + 1;
int[][] result = new int[row][col];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (j == 0){
result[i][j] = 0;
}else {
result[i][j] = col;
}
}
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (j - coins[i-1] >= 0){
result[i][j] = Math.min(result[i-1][j], 1 + result[i][j - coins[i-1]]);
}else {
result[i][j] = result[i-1][j];
}
}
}
return result[row - 1][col - 1] == col ? -1 : result[row - 1][col - 1];
}
}
4.2 优化
只需把上边的数组每一列取最小值保存下来即可。所以转化一下思路,兑换n元所需的最少硬币数,可以先求n-1元所需的最少硬币数……
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0){
return 0;
}
final int length = amount + 1;
int[] result = new int[length];
for (int i = 0; i < length; i++) {
if (i == 0){
result[i] = 0;
}else {
result[i] = length;
}
}
for (int i = 1; i < length; i++) {
for (int coin : coins) {
if (i - coin >= 0){
result[i] = Math.min(result[i], 1 + result[i - coin]);
}
}
}
return result[length- 1] == length ? -1 : result[length- 1];
}
}