0-1背包、完全背包、多重背包、零钱兑换

23 篇文章 1 订阅
16 篇文章 0 订阅

目录

一、0-1背包问题

1.1 问题描述

1.2 动态规划过程

1.3 状态转移方程

1.4 填表

1.5 代码

1.6 回溯法求解

1.7 优化

二、完全背包问题

2.1 问题描述

2.2 分析

2.3 填表

2.4 代码

2.5 回溯法求解

2.6 优化

三、多重背包

3.1 问题描述

3.2 分析

3.3 填表

3.4 代码

3.5 回溯法求解

3.6 优化

四、硬币兑换兑换

4.1 分析

4.2 优化


给定不同面额的硬币 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的背包,如何让背包里装入的物品具有最大的价值总和?

 1234
w(体积)2345
v(价值)3456

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);

由此可以得出递推关系式:

\large j<w[i]              \large V\left ( i \right,j ) = V[i-1,j]

\large j\geqslant w[i]             \large V\left ( i \right,j ) = max\left ( V[i-1][j-w[i]] + v[i], \right V[i-1][j])

为什么是前一行?

因为前一行是到达当前重量时背包内物品的最大价值,如果选择不放当前物品,说明背包容量不够,那么现在的最大价值只能是前一行的值。同理,选择放当前物品,那么也应该在前一行的基础上加上当前物品的价值。

1.4 填表

初始化边界:

 012345678
0000000000
10        
20        
30        
40        

 

根据状态转移方程填写上表:

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时,还要考虑可能需要继续放入当前物品。所以可以得到以下状态转移方程:

\large j<w[i]              \large V\left ( i \right,j ) = V[i-1,j]

\large j\geqslant w[i]             \large V\left ( i \right,j ) = max\left ( V[i][j-w[i]] + v[i], \right V[i-1][j])

2.3 填表

初始化边界:

 012345678910
000000000000
10          
20          
30          
40          

根据状态转移方程填表:

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)当 result[i][j] == result[i-1][j]时,说明没有选择第i 个商品,则回到result[i-1][j]

2) 当result[i][j] == result[i][j-w[i - 1]] + v[i - 1]时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到result[i][j-w[i - 1]]

3) 一直遍历到i=1结束为止,所有解的组成都会找到。

代码:

注意是另goods[i-1] = 0,因为当满足result[i][j] != result[i-1][j]时,回溯到的是当前物品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 的背包,如何让背包里装入的物品具有最大的价值总和?

 123
w(重量)123
v(价值)61020
num(数量)1052

多重背包和01背包、完全背包的区别:多重背包中每个物品的个数都是给定的,可能不是一个,绝对不是无限个。

3.2 分析

在0-1背包的基础上进行修改,此时在放入物品的时候就要考虑放入几个才能使价值最大,肯定是越多越好,但是又有数量的限制,所以要首先确定放入的数量。假设当前重量为j,准备要放入物品i,放入的数量为count = max(nums[i],j/w[i])

状态转移方程为:

\large j<w[i]              \large V\left ( i \right,j ) = V[i-1,j]

\large j\geqslant w[i]             \large V\left ( i \right,j ) = max\left ( V[i][j-count * w[i]] + count * v[i], \right V[i-1][j])

3.3 填表

初始化表:

 012345678
0000000000
10        
20        
30        

根据状态转移方程填表:

 012345678
0000000000
10612182430364248
20612182430364248
30612202632404652

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数组:

根据状态转移方程得到下表:

 01234567891011
00MAXMAXMAXMAXMAXMAXMAXMAXMAXMAXMAX
101234567891011
2011223344556
5011221223323

代码:

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];
    }
}

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值