【动态规划之多重背包问题】多重背包问题的通解以及空间优化(一维+扁平化优化)

198 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第28天,点击查看活动详情


⭐️前面的话⭐️

本篇文章将介绍动态规划中的背包问题——多重背包问题,前面我们已经介绍了什么是完全背包问题以及0-1背包问题,本文来介绍另外一种背包模型即多重背包问题,其实多重背包问题只是修改了一个条件,就是对每件物品的数量进行了【有限】件物品的限制而已,在解题上与0-1背包和完全背包大差不差。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!掘金首发!
📆掘金首发时间:🌴2022年10月28日🌴
✉️坚持和努力一定能换来诗与远方!
💭推荐书籍:📚《无》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


⭐️【多重背包模型】原题⭐️

🔐题目详情

NN 种物品和一个容量为 CC 的背包,每种物品 「数量有限」

ii 件物品的体积是 v[i]v[i],价值是 w[i]w[i],数量为 s[i]s[i]

问选择哪些物品,每件物品选择多少件,可使得总价值最大。

其实就是在 0-1 背包问题的基础上,增加了每件物品可以选择「有限次数」的特点(在容量允许的情况下)。

示例 1:

输入: N = 2, C = 5, v = [1,2], w = [1,2], s = [2,1]   

输出: 4

解释: 选两件物品 1,再选一件物品 2,可使价值最大。

💡解题思路与代码

🍭朴素解法

多重背包问题相比于0-1背包模型,物品可以选择多次,但又与完全背包不同,多重背包每种物品的选择次数是有限次的,其实解题的思路还是和01背包模型是一样的。

状态定义: 不妨定义f[i][j]f[i][j]表示从前ii件物品中选择,容量不超过jj的最大价值。

确定初始状态: 当只有一种物品选择时,由于数量是有限制次数的,所以在总体积满足总体积不超过背包容量和所选物品次数不超过限定次数s[0]s[0],尽量地选就行,不要超过容量jj,不妨设可以选这一种物品的最大件数为kk,则如果s[0]>=ks[0]>=k,有f[0][j]=k×w[i]f[0][j]=k \times w[i],相反如果s[0]<ks[0]<kf[0][j]=s[0]×w[i]f[0][j]=s[0] \times w[i]其中k×v[i]<=j其中k\times v[i]<=j,综合式子为:

f[0][j]=min(s[0],k)w[i],min(s[0],k)v[i]<jf[0][j]=min(s[0],k) * w[i], min(s[0],k) * v[i] < j

状态转移方程推导: 当我们选择第ii件物品的时候,我们可以选择0,1,2...s[0]0,1,2...s[0]件,不妨设所选物品的件数为kk,那么自然kk需要满足0<=k<=s[i]0<=k<=s[i], 即在不超过容量jj的情况下。

当我们不选择第ii件物品时,即k=0k=0f[i][j]=f[i1][j]f[i][j]=f[i-1][j]
当我们选择11件第ii件物品时,即k=1k=1f[i][j]=f[i1][jv[i]]+w[i]f[i][j]=f[i-1][j - v[i]] + w[i]
当我们选择22件第ii件物品时,即k=2k=2f[i][j]=f[i1][j2v[i]]+2w[i]f[i][j]=f[i-1][j - 2 * v[i]] + 2*w[i]
...
当我们选择s[i]s[i]件的第ii件物品时,即k=s[i]k=s[i]f[i][j]=f[i1][js[i]v[i]]+s[i]w[i]f[i][j]=f[i-1][j - s[i] * v[i]] + s[i]*w[i]
当我们选择kk件的第ii件物品时,即k=kk=kf[i][j]=f[i1][jkv[i]]+kw[i]f[i][j]=f[i-1][j - k * v[i]]+k*w[i]

我们所要求的是最大的价值,所以取以上所有情况的最大值即可。

综上所述,我们的状态转移方程就出来了,不妨记当前物品的价值为:

f[i][j]=max(f[i1][jkv[i]]+kw[i]),k=0,1,...s[i]f[i][j]=max(f[i-1][j-k*v[i]]+k*w[i]),k=0,1,...s[i]

实现代码:

    /**
     *
     * @param n 物品数
     * @param c 容量
     * @param v 每件物品的体积
     * @param w 每件物品的价值
     * @param s 每件物品的数量
     * @return 最大价值
     */
    public int multipleKS(int n, int c, int[] v, int[] w, int[] s) {
        //状态定义 定义f[i][j]表示选择前i件物品 在不超过背包容量j的情况下的最大价值
        int[][] f = new int[n][c + 1];

        //确地初始状态
        for (int j = 1; j <= c; j++) {
            int maxK = j / v[0];
            int k = Math.min(maxK, s[0]);
            f[0][j] = k * w[0];
        }
        //状态转移 f[i][j]=max(f[i-1][j-k*v[i]])
        for (int i = 1; i < n; i++) {
            int t = v[i];
            for (int j = 0; j <= c; j++) {
                //不选
                f[i][j] = f[i - 1][j];
                //选择k件
                for (int k = 1; k * t <= j && k <= s[i]; k++) {
                    f[i][j] = Math.max(f[i][j], f[i - 1][j - k * t] + k * w[i]);
                }
            }
        }
        return f[n - 1][c];
    }

时间复杂度:O(ncs)O(n*c*s)kk的值不会大于ss,因为每种物品只有ss件。
空间复杂度:O(c)O(c)

🍭滚动数组优化空间

根据观察我们知道第ii行的状态仅依赖与第i1i-1行的状态,因此我们可以使用滚动数组进行优化。

实现代码:

    /**
     *
     * @param n 物品数
     * @param c 容量
     * @param v 每件物品的体积
     * @param w 每件物品的价值
     * @param s 每件物品的数量
     * @return 最大价值
     */
    public int multipleKS2(int n, int c, int[] v, int[] w, int[] s) {
        //状态定义 定义f[i][j]表示选择前i件物品 在不超过背包容量j的情况下的最大价值 滚动数组优化
        int[][] f = new int[2][c + 1];

        //确地初始状态
        for (int j = 1; j <= c; j++) {
            int maxK = j / v[0];
            int k = Math.min(maxK, s[0]);
            f[0][j] = k * w[0];
        }
        //状态转移 f[i][j]=max(f[i-1][j-k*v[i]])
        for (int i = 1; i < n; i++) {
            int t = v[i];
            int pre = (i - 1) & 1;
            int cur = i & 1;
            for (int j = 0; j <= c; j++) {
                //不选
                f[cur][j] = f[pre][j];
                //选择k件
                for (int k = 1; k * t <= j && k <= s[i]; k++) {
                    f[cur][j] = Math.max(f[cur][j], f[pre][j - k * t] + k * w[i]);
                }
            }
        }
        return f[(n - 1) & 1][c];
    }

对于时空复杂度,只是优化了空间而已,所以时间复杂度不发生改变,空间复杂度优化到O(c)O(c)
时间复杂度:O(ncs)O(n*c*s)kk的值不会大于ss,因为每种物品只有ss件。
空间复杂度:O(c)O(c)

🍭一维数组优化空间

对于多重背包,它的一维优化只能够降低空间的消耗,并不能降低时间的消耗,因为多重背包不像完全背包,多重背包每件物品是有限的,而不是无限的,所以并不能像推导完全背包那样推导多重背包的状态转移方程。

多重背包和01背包一样,根据所推导的状态转移方程,第ii行第jj列的状态只依赖与上一行正上方以及前面的状态,因此我们可以采用容量维度【从大到小】遍历进行一维优化。

我们可以仅保留【容量】维度,从大到小遍历。状态转移方程如下:

f[j]=max(f[jkv[i]]+kw[i]),0<=k<=s[i]f[j]=max(f[j-k*v[i]]+k*w[i]),0<=k<=s[i]

实现代码:

    /**
     *
     * @param n 物品数
     * @param c 容量
     * @param v 每件物品的体积
     * @param w 每件物品的价值
     * @param s 每件物品的数量
     * @return 最大价值
     */
    public int multipleKS3(int n, int c, int[] v, int[] w, int[] s) {
        //状态定义 定义f[i][j]表示选择前i件物品 在不超过背包容量j的情况下的最大价值 一维数组优化
        int[] f = new int[c + 1];

        //确地初始状态
        for (int j = 1; j <= c; j++) {
            int maxK = j / v[0];
            int k = Math.min(maxK, s[0]);
            f[j] = k * w[0];
        }
        //状态转移 f[j]=max(f[j-k*t]+k*w[i] k~[0,s[i]]
        for (int i = 1; i < n; i++) {
            int t = v[i];
            for (int j = c; j >= t; j--) {
                //选择k件
                for (int k = 1; k * t <= j && k <= s[i]; k++) {
                    f[j] = Math.max(f[j], f[j - k * t] + k * w[i]);
                }
            }
        }
        return f[c];
    }

时间复杂度:O(ncs)O(n*c*s) 空间复杂度:O(c)O(c)

🍭三种背包模型的区别以及转换01背包模型优化(扁平化处理)

现在我们来回顾一下三种模型:

  • 【0-1背包模型】给定nn件物品,每件物品只有11件,每件物品体积为v[i]v[i],每件物品价值为w[i]w[i],背包容量为cc,求最大容量。
  • 【完全背包模型】给定nn件物品,每件物品有无数无数件,每件物品体积为v[i]v[i],每件物品价值为w[i]w[i],背包容量为cc,求最大容量。
  • 【0-1背包模型】给定nn件物品,每件物品有s[i]s[i]件,每件物品体积为v[i]v[i],每件物品价值为w[i]w[i],背包容量为cc,求最大容量。

三种背包只有每件物品的数量条件是不同的,其余的条件以及问题都是一模一样的,所以使用动态规划解决时,状态定义均为:f[i][j]f[i][j]表示从前ii件物品中选择,容量不超过jj的最大价值。

状态转移方程分别为: 0-1背包: 朴素方程:

f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])

一维优化方程:【容量维度从大到小遍历】

f[j]=max(f[j],f[jv[i]]+w[i])f[j]=max(f[j],f[j-v[i]]+w[i])

完全背包: 朴素方程:

f[i][j]=max(f[i1][jkv[i]]+kw[i]),k=0,1,2,...f[i][j]=max(f[i-1][j-k*v[i]]+k*w[i]),k=0,1,2,...

一维优化方程:【容量维度从小到大遍历】

f[j]=max(f[j],f[jv[i]]+w[i])f[j]=max(f[j],f[j-v[i]]+w[i])

多重背包: 朴素方程:

f[i][j]=max(f[i1][jkv[i]]+kw[i]),k=0,1,2,...,s[i]f[i][j]=max(f[i-1][j-k*v[i]]+k*w[i]),k=0,1,2,...,s[i]

一维优化方程:【容量维度从大到小遍历】

f[j]=max(f[jkv[i]]+kw[i]),k=0,1,2,...,s[i]f[j]=max(f[j-k*v[i]]+k*w[i]),k=0,1,2,...,s[i]

其实多重背包模型可以理解为一种特殊的【0-1】背包模型,我们可以将物品扁平化转换为0-1背包问题,扁平化的意思你可以理解为,本来有若干件相同物品是捆在一起的,现在将这些物品解绑,一件一件摆出来并视作为不同物品,这就是物品的扁平化,这样我们问题也就转换成0-1背包问题了。

    /**
     *
     * @param n 物品数
     * @param c 容量
     * @param v 每件物品的体积
     * @param w 每件物品的价值
     * @param s 每件物品的数量
     * @return 最大价值
     */
    public int multipleKS4(int n, int c, int[] v, int[] w, int[] s) {
        //状态定义 定义f[i][j]表示选择前i件物品 在不超过背包容量j的情况下的最大价值 扁平化转换为0-1背包
        //使用list储存<v,w>,有多少个物品就存储多少个

        List<int[]> list = new ArrayList<>();
        //扁平化
        for (int i = 0; i < n; i++) {
            int cnt = s[i];
            while (cnt-- > 0) {
                list.add(new int[]{v[i], w[i]});
            }
        }
        int[] f = new int[c + 1];

        //确定初始状态 0-1模型 如果j>=vi0 则f[j]=wi0 其实可以不用初始化 在后面状态转移可以包含了初始状态过程
        //状态转移
        for (int i = 0; i < list.size(); i++) {
            int vi = list.get(i)[0];
            int wi = list.get(i)[1];
            for (int j = c; j >= vi; j--) {
                //选择与不选择选择价值大的
                f[j] = Math.max(f[j], f[j - vi] + wi);
            }
        }
        return f[c];
    }

时间复杂度:O(0n1s[i]c)O(\sum_0^{n-1}s[i]*c)
空间复杂度:O(0n1s[i]+c)O(\sum_0^{n-1}s[i] + c)

🌱总结

本篇文章介绍了多重背包的朴素解题方法,滚动数组优化,一维数组优化以及扁平化优化,这几种优化方式只能优化空间复杂度,而做不到优化时间复杂度,那有没有优化时间复杂度的方法呢?答案是有的,这个问题我们留在下一篇博客,可以使用单调队列进行优化。
除此之外,我们还总结了三种基本背包模型的区别以及状态转移方程的区别。


参考资料: 宫水三叶背包问题

觉得文章写得不错的老铁们,点赞评论关注走一波!谢谢啦!