给定N个物品的背包问题-动态规划

给定 N N N 个物品,所有物品的体积恰有 K K K ( K ≤ N ) (K \leq N) (KN),第 i i i 种物品的体积为 V i ( 0 < V i < 1 ) V_i (0 < V_i < 1) Vi(0<Vi<1) 且第 i i i 种物品的个数为 N i ( 1 ≤ i ≤ K ) N_i(1 \leq i \leq K) Ni(1iK),其中 N 1 + N 2 + ⋯ + N k = N N_1 + N_2 + \dots + N_k = N N1+N2++Nk=N。现有无穷多个体积为 1 的箱子,要将这 N N N 个物品装箱。试设计动态规划算法输出一个装箱方案,使得所用的箱子数最少。

给定N个物品的背包问题

一个有趣的反向背包问题,题目描述如上。
普通的0-1背包问题是 背包容量固定,尽可能放入更多的物品
而这个题目则恰好相反,物品数量固定,尽可能少用背包(箱子)

这里给出了思路分析和递归代码实现。

动态规划思路

显然可以用动态规划解决这个问题(题目也是如此要求),但是这个题目的动态规划过程要比普通的0-1背包问题复杂很多。
既然要用动态规划,核心问题仍然是找到最优子结构重叠子问题,然后写出状态转移方程

首先做如下设定:

  1. 假设已经将前 i − 1 i - 1 i1 个物品放入了一些箱子中,这些目前已占用的箱子的集合为 S i S_i Si (并不一定是最优的放置方式),共为 n i n_i ni 个箱子。
  2. 在此基础上,将从第 i i i 个物品到第 N N N 个物品(也就是后面的 N − i + 1 N - i + 1 Ni+1 个物品)放入箱子中,最后共需占用的箱子数的最小值 m i m_i mi
  3. 将第 i i i 个物品放入 S i S_i Si 中的第 k k k 个箱子表示为 S i [ k ] ← i S_i[k] \leftarrow i Si[k]i ,其中 k = − 1 , 0 , 1 , … , n i k = -1, 0, 1, \dots, n_i k=1,0,1,,ni k = − 1 k = -1 k=1 表示不将 i i i 放入任何已有的箱子,而是将其放入一个新的箱子中)。

基于以上设定,子问题就可以定义为 m i m_i mi ,而 m 0 m_0 m0 就是整个问题的最终答案。

由此,可以写出状态转移方程:
m i = min ⁡ { 当 S i [ k ] ← i 时 的 m i + 1    ∣    k = − 1 , 0 , 1 , … , n i } m_i = \min \{ 当 S_i[k] \leftarrow i 时的 m_{i + 1} \ \ | \ \ k = -1, 0, 1, \dots, n_i \} mi=min{Si[k]imi+1    k=1,0,1,,ni}
该状态转移方程的意义是,把当前的物品 i i i 放入第 k k k 个箱子,然后计算后面 N − i N - i Ni 个物品放入箱子后所得到的最优解(最小的共需箱子数) m i + 1 m_{i+ 1} mi+1 。于是, k k k 取某个值时,能够使得 m i + 1 m_{i+ 1} mi+1 最小,而 m i m_i mi 即等于这个最小的 m i + 1 m_{i+ 1} mi+1 值。

Java代码实现

基于上面的子问题定义和状态转移方程,即可写出递归实现的代码。
需要考虑使用什么数据结构来实现目前已占用的箱子的集合为 S i S_i Si ,这里使用数组实现,不仅节省内存空间,使用起来也较为方便。

完整的Java代码如下:

public class Main {
    private double[] boxSet;  //目前已占用的箱子的集合,用数组实现
    private int currentBoxNum = 0;  //目前已占用的箱子的数量(实际上是boxSet中最后一个不为空的箱子的下标,需加 1 才表示真正的已占用箱子数量)

    public static void main(String[] args) {
        double[] items = new double[]{0.6, 0.5, 0.3, 0.2, 0.2, 0.2};
        Main main = new Main();
        System.out.println("需要的最小箱子数:" + main.minBoxesInNeed(items));
    }

    /**
     * 计算将 items 中的物品全部放入体积为 1 的箱子中所需的最小箱子数量
     *
     * @param items 所有物品的体积的列表,如 {0.6, 0.5, 0.3, 0.2, 0.2, 0.2}
     * @return 所有物品全部放入箱子中所需的最小的箱子数
     */
    public int minBoxesInNeed(double[] items) {
        boxSet = new double[items.length];
        return minBoxesInNeedFromI(items, 0);
    }

    /**
     * 计算在已经占用了 currentBoxNum 个箱子的情况下,将 items 中从第 i 个物品开始,到最后一个物品,全部放入箱子中,共需要的箱子的最小数量
     *
     * @param items 所有物品的体积的列表,如 {0.6, 0.5, 0.3, 0.2, 0.2, 0.2}
     * @param i     第 i 个物品的下标
     * @return 在已经占用了 currentBoxNum 个箱子的情况下,把从第 i 个物品开始,到最后一个物品,全部放入箱子中,最后共需要的箱子的最小数量
     */
    private int minBoxesInNeedFromI(double[] items, int i) {
        int minNum = Integer.MAX_VALUE;  //把从 i 开始,后面所有物品放入箱子中,所需要的最小的箱子数量
        for (int k = currentBoxNum; k >= -1; k--) {  //从大到小遍历,防止遍历 b = -1 时 currentBoxNum 的加一操作导致遍历次数多一轮
            boolean putSuccess = putIntoBox(k, items[i]);
            if (putSuccess) {
                int boxesInNeedAfterI;  //把从 i + 1 开始,后面所有物品放入箱子中,需要的箱子数量

                if (i == items.length - 1) {  //若已经是最后一个物品,则返回目前使用的箱子数,也即是共使用的箱子的总数
                    boxesInNeedAfterI = currentBoxNum + 1;
                    deleteFromBox(k, items[i]);  //把当前物品从箱子中取出,防止影响后续的递归
                    return boxesInNeedAfterI;
                }

                boxesInNeedAfterI = minBoxesInNeedFromI(items, i + 1);  //递归
                if (minNum > boxesInNeedAfterI) minNum = boxesInNeedAfterI;
                deleteFromBox(k, items[i]);  //把当前物品从箱子中取出,防止影响后续的递归
            }
        }
        return minNum;
    }

    /**
     * 将一个体积为 volume 的物品放入下标为 boxIndex 的箱子中,若 boxIndex = -1 则放入一个新箱子中
     *
     * @param boxIndex 当前物品要放入的箱子的下标,若为 -1 则放入一个新的箱子中,并将 currentBoxNum 加一
     * @param volume 要放入的物品的体积
     * @return 是否成功将物品放入了第 boxIndex 个箱子中
     */
    private boolean putIntoBox(int boxIndex, double volume) {
        if (boxIndex == -1) {
            currentBoxNum++;
            boxSet[currentBoxNum] += volume;
            return true;
        } else {
            if (boxSet[boxIndex] + volume <= 1) {
                boxSet[boxIndex] = boxSet[boxIndex] + volume;
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * 从下标为 boxIndex 的箱子中删除体积为 volume 的物品,若 boxIndex = -1 则删除最后一个箱子中的物品
     *
     * @param boxIndex 当前物品要删除物品的箱子的下标,若为 -1 则删除最后一个箱子中的物品,并将 currentBoxNum 加一
     * @param volume 要删除的物品的体积
     */
    private void deleteFromBox(int boxIndex, double volume) {
        if (boxIndex == -1) {
            boxSet[currentBoxNum] -= volume;
            currentBoxNum--;
        } else {
            boxSet[boxIndex] -= volume;
        }
    }
}

这里只进行了递归实现,若能改为循环实现可以获得更好的鲁棒性,但是由于目前已占用的箱子的集合为 S i S_i Si 的存在,目前未想出循环实现的方法。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZBH4444

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值