动态规划算法:背包问题

1,应用场景:背包问题

  • 问题描述:有一个容量为4磅的背包,需要装入如列表下的物品,在装入物品可重复和不可重复两种场景下,怎样才能使装入机制最大化
商品名称商品重量商品价格
吉他11500
音响43000
电脑32000

2,动态规划算法描述

  • 动态规划算法(Dynamic Programming)核心的思想是:将大问题划分为小问题进行解决,从而 一步步 获得最优解的处理算法(这个一步步是重点,等会就发现真是一步步)
  • 动态规范算法和分治算法类似,基本思想都是将待解决问题分解成若干个子问题,先求解子问题,然后从子问题中得到原问题的解
  • 与分治算法不同的是,适用于动态规划求解的问题,经分解得到的子问题往往不是相互独立的,下一个子阶段的求解是建立在上一个解决的求解基础上的,依次递进获取最终解
  • 动态规划可以通过填表的方式来逐步推进,最终得到最优解

3,动态规划算法的最佳实践——背包问题

  • 背包问题,就是给定一个容量的背包,依次放入物品,装入物品可从可重复和不可重复两个维度分析;装入物品总重量不能超过背包容量
  • 在这个过程中,可以通过一个二维表格来分析:
    • 横行表示背包容量从0到指定容量的各种情况,这是第一步的分,将大容量的背包先转化为小容量背包,算出子问题的最优解,然后一步步加大容量,算出最终问题的最优解
    • 纵行表示商品信息,且第一横行为空值,作为初始数据的对比值;纵行是第二步的分,先将一个商品放入背包中,算出最优解,逐渐增加商品类型和商品数量,算出最终最优解
    • 最终表格的最右下角的格子,即为数据的最优解

3.1,不可重复表格填充演示

物品/背包容量0磅1磅2磅3磅4磅
第一横行000000
吉他|1|150001500150015001500
音响|4|300001500150015003000
电脑|3|200001500150020003500
  • 第一横行和第一纵行统一初始化为0,作为后续数据的初始化对比
  • 开始填入物品,从第二横行开始,此时只有一个物品能填入背包,在填入时用物品重量和背包容量进行比较,能填充进去,直接填充并修改背包价值,不能填充进入则背包容量依旧为0,因为不可重复,不考虑放两个一样的,则对于第二横行来说, 吉他重量为1,在背包无论容量是多少,最大价值都是1500
  • 继续填充第三横行,此时可装入背包的物品变为两个,则开始存在判断
    • 如果当前物品重量大于背包容量,则不能装入,背包价值沿用该列上一行的价值,即table[i-1][j],适用于第三横行的前三磅;
    • 如果当前物品重量大于等于背包容量,此处为一种情况,分析中可以分两部分分析,先进行等于分析,装入第四行时进行大于分析
    • 如果当前商品重量等于背包容量,则背包可以装入该商品,但是需要对场景进行分析,此时背包已经装入了其他商品,把其他商品清空,装入该商品后,价值不一定大于原价值,所以需要进行比较,取Max(customerPrvice + 0, table[i-1][j]),即用当前商品价值与该列上一行的价值进行比较,取最大值为当前背包价值,此处商品加0是因为相等,大于第四行分析;所以在第三种第四列中,4磅背包容量时,可以装入音响,商品价值为3000 > 原来的1500,装入3000
  • 继续填充第四横行,因为商品重量为3磅,所以0-2磅不符合,直接取上一行的值;3磅时,电脑价值3000 > 原来价值1500,填充为电脑价值,重要的是第四列,背包容量大于当前商品容量时:
    • 背包容量大于当前商品容量,首先必须装入当前商品,依次来获取一个比对价值与上一行价值进行比较来获取一个最优解,但是在装入当前商品时,背包还有容量,此时4 - 3 = 1磅,背包还可以装入1磅的商品,则需要从上一行背包容量为1磅的位置,取出背包此时的最高价值与当前商品价值相加,形成在该位置处的当前商品所能带来的最高价值,然后在于该列上一行进行比较,即Max(customerPrice + table[i-1][packageCapacity - customerWeight], table[i-1][j]);此时放入本商品的价值是2000 + 1500 = 3500,本列上一行的价值是3000,则直接覆盖
    • customerPrice:当前商品价格,customerWeight:当前商品重量,packageCapacity:背包容量,packageCapacity - customerWeight:背包剩余重量,table[i-1][packageCapacity - customerWeight]:背包在当前商品类型剩余重量下的最优解
    • 此处必须从上一行找剩余容量的最优解,原因在于商品不能重复,如果从本行找最优解,可能剩余容量的最优解中已经包含了当前商品

3.2,可重复表格填充演示

物品/背包容量0磅1磅2磅3磅4磅
第一横行000000
吉他|1|150001500300045006000
音响|4|300001500300045006000
电脑|3|200001500300045006000
  • 可重复放入与不可重复放入逻辑基本一致,不过有一个点需要注意,就是在背包容量大于商品重量是,需要在当前商品行找剩余重量的最优解,而不是上一行,因为商品可以重复!
  • 依次逻辑填充第二横行,因为吉他重量是1磅,在1磅时,价值为1500;2磅是,剩余容量为1磅,则取1磅的最优解1500,价值为3000;3磅是,剩余价值为2磅,取2磅最优解3000,价值为4500;4磅为6000
  • 继续填充第三横行,前三磅分别取上一行最优解;4磅处,用3000与6000取最大值6000
  • 继续填充第四横行,前两磅分别取上一行最优解;3磅处,用2000与3000取最大值3000;4磅处,用2000 + 1500 = 3500与6000取最大值6000
  • 动态规划背包问题算法解析基本完成,可以看到,就是将问题尽量划小,随着背包容量不断增加,商品种类不断增多,来最终获取到预期的最优解

4,代码实现

package com.self.datastructure.algorithm.dynamic;

import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;

/**
 * 动态规划: 背包问题
 * 问题: 有一个背包, 容量为4磅, 现在有三种不同重量不同价值商品, 怎样放能让背包价值更大
 * 吉他     1磅   1500
 * 音响     4磅   3000
 * 电脑     3磅   2000
 * @author PJ_ZHANG
 * @create 2020-07-02 12:29
 **/
public class KnapsackProblem {

    public static void main(String[] args) {
        // 商品信息, 数组索引相对应
        // 名称数组
        String[] nameArr = {"吉他", "音响", "电脑"};
        // 重量数组
        int[] weightArr = {1, 4, 3};
        // 价格数组
        int[] priceArr = {1000, 3000, 6000};
        // 背包容量
        int packageCapacity = 6;
        // 不可重复
        backpackWithoutRepeat(nameArr, weightArr, priceArr, packageCapacity);
        // 可重复, 与不可重复变更不大, 只对关键部分注释
        backpackWithRepeat(nameArr, weightArr, priceArr, packageCapacity);
    }


    /**
     * 装入背包
     * @param nameArr 名称数组
     * @param weightArr 重量数组
     * @param priceArr 价格数组
     * @param packageCapacity 背包容量
     */
    private static void backpackWithRepeat(String[] nameArr, int[] weightArr, int[] priceArr, int packageCapacity) {
        int[][] packageArr = new int[nameArr.length + 1][packageCapacity + 1];
        // 不可重复的统计中, 使用int[][]二维数组, 通过标志位进行记录
        // 并在后续遍历过程后, 通过倒叙遍历, 依次取出参与背包容量的最优解
        // 但是在可重复的统计中, 在最优解中, 可能存在多种商品参与多次, 此处直接用String进行拼接
        String[][] contentArr = new String[nameArr.length + 1][packageCapacity + 1];
        for (int i = 1; i < packageArr.length; i++) {
            for (int j = 1; j < packageArr[i].length; j++) {
                if (weightArr[i - 1] > j) {
                    packageArr[i][j] = packageArr[i - 1][j];
                } else {
                    int onePart = packageArr[i - 1][j];
                    // 此处取当前行的前置价格
                    // 在当前行中, 可能剩余重量代表的最优解中, 已经包含了该商品, 所以会重复
                    int otherPart = priceArr[i - 1] + packageArr[i][j - weightArr[i - 1]];
                    packageArr[i][j] = Math.max(onePart, otherPart);
                    // 商品已经使用, 通过String数组对名称进行记录
                    // 取上一行的最优解, 直接复制引用的名称串
                    if (packageArr[i][j] == onePart) {
                        contentArr[i][j] = contentArr[i - 1][j];
                    } else {
                        // 通过当前商品加剩余重量拼接形成最优解
                        // 则同样用当前商品名称加上剩余重量处的引用名称组成新的名称引用传
                        contentArr[i][j] = nameArr[i - 1] + "," +
                                (StringUtils.isEmpty(contentArr[i][j - weightArr[i - 1]])
                                        ? ""
                                        : contentArr[i][j - weightArr[i - 1]]);
                    }
                }
            }
        }
        // 最终的背包价值和背包内容就是各自二维数组的最后一个元素
        // 背包价值
        System.out.println("背包价值: " + packageArr[nameArr.length][packageCapacity]);
        // 背包内容,
        System.out.println("背包内容: " + contentArr[nameArr.length][packageCapacity]);
    }


    /**
     * 装入背包
     * @param nameArr 名称数组
     * @param weightArr 重量数组
     * @param priceArr 价格数组
     * @param packageCapacity 背包容量
     */
    private static void backpackWithoutRepeat(String[] nameArr, int[] weightArr, int[] priceArr, int packageCapacity) {
        // 构建背包重量从0到指定重量对应的价格最优解二维数组
        // 该数组是对背包从0到指定重量的所有重点上价格最优解的罗列
        // 第一维表示商品, 并空出第一行, 初始化为0
        // 第二维表示背包重量从0到指定重量,
        // 值表示在当前背包容量和现有商品条件下, 背包包含价值的最优解
        // 如果商品数量是(50), 背包容量是(100),
        // 值(int[i][j])表示在当前背包容量下, 前i个商品在j背包容量时的最优解(不是所有商品和最大容量)
        // int数组初始化为0, 所以不用刻意处理, 空出即可
        int[][] packageArr = new int[nameArr.length + 1][packageCapacity + 1];
        // 构建可能装入背包的二维数组
        // 第一位表示商品, 第二维表示背包重量, 即在背包容量为j时, 当前商品i有没有可能装入背包
        // 此时一个商品可能在各个重量段被装入背包, 注意后续遍历获取逻辑
        // 值为0时说明不会装进背包, 值为1说明可能装入背包, 最终最优解可能不会装入
        int[][] contentArr = new int[nameArr.length + 1][packageCapacity + 1];
        // 装入背包基本计算公式
        // 如果当前商品重量大于当前遍历到的背包容量, 则把当前列(重量列)的上一行值(也可能是取的上一行值)赋给该值
        // 即 int[i][j] = int[i - 1][j]
        // 如果当前商品重点小于等于遍历到的背包容量, 则对两部分内容进行比较
        // 第一部分: 该列上一行的最优解
        // 第二部分: 当前商品价值 +  上一行在(总重量-当前商品重量)处的最优解
        // 开始遍历, 先遍历第一维, 即商品维度, 从1开始遍历, 跳过第一行
        for (int i = 1; i < packageArr.length; i++) {
            // 再遍历第二维, 即背包容量维度, 从1开始遍历, 跳过第一列
            for (int j = 1; j < packageArr[i].length; j++) {
                // 对商品重量和背包容量(j)进行比较
                if (weightArr[i - 1] > j) {
                    // 当前商品 > 背包容量, 取同列上一行数据
                    packageArr[i][j] = packageArr[i - 1][j];
                    // 因为不存在商品装入, 不对contentArr进行处理
                } else {
                    // 当前商品 <= 背包容量, 对两部分内容进行比较
                    // 第一部分, 该列上一行数据
                    int onePart = packageArr[i - 1][j];
                    // 第二部分, 当前商品 + 上一行在(总重量-当前商品重量)处的最优解
                    // priceArr[i - 1]: 当前商品价格
                    // weightArr[i - 1]: 当前商品重量
                    // j - weightArr[i - 1]: 去掉当前商品, 背包剩余容量
                    // 不可重复: packageArr[i - 1][j - weightArr[i - 1]]: 在上一行, 取剩余重量下的价格最优解
                    // 根据是否可以重复取横坐标
                    int otherPart = priceArr[i - 1] + packageArr[i - 1][j - weightArr[i - 1]];
                    // 取最大值为当前位置的最优解
                    packageArr[i][j] = Math.max(onePart, otherPart);
                    // 如果最优解包含当前商品, 则表示当前商品已经被使用, 进行记录
                    if (otherPart == packageArr[i][j]) {
                        contentArr[i][j] = 1;
                    }
                }
            }
        }

        // 不能重复的场景中
        // 如果该位置的标志位为1, 说明该商品参与了最终的背包添加
        // 如果该位置的标志位为0, 即使该位置的价格为最大价格, 也是从其他位置引用的价格
        // 因为不能重复, 所以每行只取一个数据参与最终计算, 并只判断在最大位置该商品是否参与
        // 该最大位置会随着已经遍历出其他元素而对应不断减小, 直到为0

        // 二维数组最后一个元素必然是最大值, 但是需要知道该最大值是自身计算的 还是比较后引用其他的
        int totalPrice = 0;
        // 最大行下标数, 即商品数
        int maxLine = contentArr.length - 1;
        // 最大列下标数, 即重量
        int maxColumn = contentArr[0].length - 1;
        for (;maxLine > 0 && maxColumn > 0;) {
            // 等于1表示在该位置该商品参与了计算
            if (contentArr[maxLine][maxColumn] == 1) {
                // 遍历后, 对重量减少, 下一次从剩余重量中取参与商品
                maxColumn -= weightArr[maxLine - 1];
                totalPrice += priceArr[maxLine - 1];
                System.out.printf("%s 加入了背包 \n", nameArr[maxLine - 1]);
            }
            // 因为不能重复
            // 所以如果该商品参与了背包容量, 则肯定剩余的最大位置处参与,
            // 否则跟该数据无关, 直接跳过
            maxLine--;
        }
        System.out.println("不重复情况下, 背包可容纳的最大价值: " + totalPrice);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值