前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【动态规划/背包问题】分组背包问题练习篇

【动态规划/背包问题】分组背包问题练习篇

作者头像
宫水三叶的刷题日记
发布2021-07-22 10:00:26
发布2021-07-22 10:00:26
1.3K00
代码可运行
举报
运行总次数:0
代码可运行

前言

今天是我们讲解「动态规划专题」中的「背包问题」的第十三篇

今天将完成一道「分组背包」练习题。

由于 LeetCode 没有与「分组背包求最大价值」相关的题目,因此我们使用「分组背包求方案数」来作为练习篇。

另外,我在文章结尾处列举了我所整理的关于背包问题的相关题目。

背包问题我会按照编排好的顺序进行讲解(每隔几天更新一篇,确保大家消化)。

你可以先尝试做做,也欢迎你向我留言补充,你觉得与背包相关的 DP 类型题目 ~

题目描述

这是 LeetCode 上的「1155. 掷骰子的N种方法」,难度为「中等」

Tag : 「背包问题」、「动态规划」、「分组背包」

这里有 d 个一样的骰子,每个骰子上都有 f 个面,分别标号为 1,2,...,f

我们约定:掷骰子的得到总点数为各骰子面朝上的数字的总和。

如果需要掷出的总点数为 target,请你计算出有多少种不同的组合情况(所有的组合情况总共有

f^d

种),模

10^9 + 7

后返回。

示例 1:

代码语言:javascript
代码运行次数:0
运行
复制
输入:d = 1, f = 6, target = 3

输出:1

示例 2:

代码语言:javascript
代码运行次数:0
运行
复制
输入:d = 2, f = 6, target = 7

输出:6

示例 3:

代码语言:javascript
代码运行次数:0
运行
复制
输入:d = 2, f = 5, target = 10

输出:1

示例 4:

代码语言:javascript
代码运行次数:0
运行
复制
输入:d = 1, f = 2, target = 3

输出:0

示例 5:

代码语言:javascript
代码运行次数:0
运行
复制
输入:d = 30, f = 30, target = 500

输出:222616187

提示:

  • 1 <= d, f <= 30
  • 1 <= target <= 1000

分组背包

分组背包问题 中我们提到,分组背包不仅仅有「组内物品最多选择一个」的情况,还存在「组内物品必须选择一个」的情况。

对于本题,可以将每个骰子看作一个物品组,且每次 必须 从物品组中选择一个物品(所掷得的数值大小视作具体物品)。

这样就把问题转换为:

d

个骰子(物品组)进行掷,掷出总和(取得的总价值)为

t

的方案数。

虽然,我们还没专门讲过「背包问题求方案数」,但基本分析与「背包问题求最大价值」并无本质区别。

我们可以套用「分组背包求最大价值」的状态定义来微调:

f[i][j]

表示考虑前

i

个物品组,凑成价值为

j

的方案数。

为了方便,我们令物品组的编号从

1

开始,因此有显而易见的初始化条件

f[0][0] = 1

代表在不考虑任何物品组的情况下,只有凑成总价值为

0

的方案数为

1

,凑成其他总价值的方案不存在。

不失一般性考虑

f[i][j]

该如何转移,也就是考虑第

i

个物品组有哪些决策。

根据题意,对于第

i

个物品组而言,可能决策的方案有:

i

个骰子的结果为

1

,有

f[i][j] = f[i - 1][j - 1]
i

个骰子的结果为

2

,有

f[i][j] = f[i - 1][j - 2]

...

i

个骰子的结果为

m

,有

f[i][j] = f[i - 1][j - m]
f[i][j]

则是上述所有可能方案的方案数总和,即有:

f[i][j] = \sum_{k = 1}^{m}f[i - 1][j - k], j >= k
朴素二维

代码:

代码语言:javascript
代码运行次数:0
运行
复制
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[][] f = new int[n + 1][t + 1];
        f[0][0] = 1;
        // 枚举物品组(每个骰子)
        for (int i = 1; i <= n; i++) {
            // 枚举背包容量(所掷得的总点数)
            for (int j = 0; j <= t; j++) {
                // 枚举决策(当前骰子所掷得的点数)
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[i][j] = (f[i][j] + f[i-1][j-k]) % mod;
                    }
                }
            }
        } 
        return f[n][t];
    }
}
  • 时间复杂度:
O(n * m * t)
  • 空间复杂度:
O(n * t)
滚动数组

根据状态转移方程,我们发现

f[i][j]

明确只依赖于

f[i - 1][x]

,且

x < j

因此我们可以使用之前学过的「滚动数组」,用很机械的方式将空间从

O(n * t)

优化至

O(t)

需要注意的是,由于我们直接是在

f[i][j]

格子的基础上进行方案数累加,因此在计算

f[i][j]

记得手动置零。

代码:

代码语言:javascript
代码运行次数:0
运行
复制
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[][] f = new int[2][t + 1];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int a = i & 1, b = (i - 1) & 1;
            for (int j = 0; j <= t; j++) {
                f[a][j] = 0; // 先手动置零
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[a][j] = (f[a][j] + f[b][j-k]) % mod;
                    }
                }
            }
        } 
        return f[n&1][t];
    }
}
  • 时间复杂度:
O(n * m * t)
  • 空间复杂度:
O(t)
一维空间优化

更进一步,利用「

f[i][j]

明确只依赖于

f[i - 1][x]

,且

x < j

」,我们能通过「01 背包」一维空间优化方式:将物品维度取消,调整容量维度遍历顺序为「从大到小」。

代码:

代码语言:javascript
代码运行次数:0
运行
复制
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[] f = new int[t + 1];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = t; j >= 0; j--) {
                f[j] = 0;
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[j] = (f[j] + f[j-k]) % mod;
                    }
                }
            }
        } 
        return f[t];
    }
}
  • 时间复杂度:
O(n * m * t)
  • 空间复杂度:
O(t)

总结

不难发现,不管是「组内物品最多选一件」还是「组内物品必须选一件」。

我们都是直接套用分组背包基本思路「枚举物品组-枚举容量-枚举决策」进行求解。

分组背包的空间优化并不会降低时间复杂度,所以对于分组背包问题,我们可以直接写方便调试的朴素多维版本(在空间可接受的情况下),如果遇到卡空间,再通过机械的方式改为「滚动数组」形式。

另外今天我们使用「分组背包问题求方案数」来作为「分组背包问题求最大价值」的练习题。

可以发现,两者其实并无本质区别,都是套用「背包问题求最大价值」的状态定义来微调。

更多的关于「背包问题求方案数」相关内容,在后面也会继续细讲。

背包问题(目录)

  1. 01背包 : 背包问题 第一讲
    1. 【练习】01背包 : 背包问题 第二讲
    2. 【学习&练习】01背包 : 背包问题 第三讲
  2. 完全背包 : 背包问题 第四讲
    1. 【练习】完全背包 : 背包问题 第五讲
    2. 【练习】完全背包 : 背包问题 第六讲
    3. 【练习】完全背包 : 背包问题 第七讲
  3. 多重背包 : 背包问题 第八讲
  4. 多重背包(优化篇)
    1. 【上】多重背包(优化篇): 背包问题 第九讲
    2. 【下】多重背包(优化篇): 背包问题 第十讲
  5. 混合背包 : 背包问题 第十一讲
  6. 分组背包 : 背包问题 第十二讲
    1. 【练习】分组背包 : 本篇
  7. 多维背包
    1. 【练习】多维背包
  8. 树形背包
    1. 【练习篇】树形背包
  9. 背包求方案数
    1. 【练习】背包求方案数
  10. 背包求具体方案
    1. 【练习】背包求具体方案
  11. 泛化背包
    1. 【练习】泛化背包

最后

这是我们「刷穿 LeetCode」系列文章的第 No.1155 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 宫水三叶的刷题日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 题目描述
  • 分组背包
    • 朴素二维
    • 滚动数组
    • 一维空间优化
  • 总结
  • 背包问题(目录)
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档