【背包问题の第二讲】如何将原问题抽象为「01 背包」问题

简介: 【背包问题の第二讲】如何将原问题抽象为「01 背包」问题

前言



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


在众多背包问题中「01 背包问题」是最为核心的,因此我建议你先精读过 背包问题 第一讲 之后再阅读本文。


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


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


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


题目描述



这是 LeetCode 上的416. 分割等和子集,难度为 Medium


给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。


注意:


  • 每个数组中的元素不会超过 100
  • 数组的大小不会超过 200


示例 1:


输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
复制代码


示例 2:


输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
复制代码


基本分析



通常「背包问题」相关的题,都是在考察我们的「建模」能力,也就是将问题转换为「背包问题」的能力。


由于本题是问我们能否将一个数组分成两个「等和」子集。


问题等效于能否从数组中挑选若干个元素,使得元素总和等于所有元素总和的一半


这道题如果抽象成「背包问题」的话,应该是:


我们背包容量为 target=sum/2target=sum/2target=sum/2,每个数组元素的「价值」与「成本」都是其数值大小,求我们能否装满背包。


转换为 01 背包



由于每个数字(数组元素)只能被选一次,而且每个数字选择与否对应了「价值」和「成本」,求解的问题也与「最大价值」相关。


可以使用「01 背包」的模型来做。


当我们确定一个问题可以转化为「01 背包」之后,就可以直接套用「01 背包」的状态定义进行求解了。


注意,我们积累 DP 模型的意义,就是在于我们可以快速得到可靠的「状态定义」。


路径问题 中我教过你通用的 DP 技巧解法,但那是基于我们完全没见过那样的题型才去用的,而对于一些我们见过题型的 DP 题目,我们应该直接套用(或微调)该模型「状态定义」来做。


我们直接套用「01 背包」的状态定义:


f[i][j]f[i][j]f[i][j] 代表考虑前 iii 个数值,其选择数字总和不超过 jjj 的最大价值。


当有了「状态定义」之后,结合我们的「最后一步分析法」,每个数字都有「选」和「不选」两种选择。


因此不难得出状态转移方程:


网络异常,图片无法展示
|


代码:


class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;
        // 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
        if (target * 2 != sum) return false;
        int[][] f = new int[n][target + 1];
        // 先处理考虑第 1 件物品的情况
        for (int j = 0; j <= target; j++) {
            f[0][j] = j >= nums[0] ? nums[0] : 0;
        }
        // 再处理考虑其余物品的情况
        for (int i = 1; i < n; i++) {
            int t = nums[i];
            for (int j = 0; j <= target; j++) {
                // 不选第 i 件物品
                int no = f[i-1][j];
                // 选第 i 件物品
                int yes = j >= t ? f[i-1][j-t] + t : 0;
                f[i][j] = Math.max(no, yes);
            }
        }
        // 如果最大价值等于 target,说明可以拆分成两个「等和子集」
        return f[n-1][target] == target;
    }
}
复制代码


  • 时间复杂度:targettargettarget 为数组总和的一半,nnn 数组元素个数。为共有 n∗targetn * targetntarget 个状态需要被转移,复杂度为 O(n∗target)O(n * target)O(ntarget)
  • 空间复杂度:O(n∗target)O(n * target)O(ntarget)


滚动数组



在上一讲我们讲到过「01 背包」具有两种空间优化方式。


其中一种优化方式的编码实现十分固定,只需要固定的修改「物品维度」即可。


代码:


class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;
        if (target * 2 != sum) return false;
        // 将「物品维度」修改为 2
        int[][] f = new int[2][target + 1];
        // 先处理考虑第 1 件物品的情况
        for (int j = 0; j <= target; j++) {
            f[0][j] = j >= nums[0] ? nums[0] : 0;
        }
        // 再处理考虑其余物品的情况
        for (int i = 1; i < n; i++) {
            int t = nums[i];
            for (int j = 0; j <= target; j++) {
                // 不选第 i 件物品,将物品维度的使用加上「&1」
                int no = f[(i-1)&1][j];
                // 选第 i 件物品,将物品维度的使用加上「&1」
                int yes = j >= t ? f[(i-1)&1][j-t] + t : 0;
                f[i&1][j] = Math.max(no, yes);
            }
        }
        // 如果最大价值等于 target,说明可以拆分成两个「等和子集」
        // 将物品维度的使用加上「&1」
        return f[(n-1)&1][target] == target;
    }
}
复制代码


  • 时间复杂度:targettargettarget 为数组总和的一半,nnn 数组元素个数。为共有 n∗targetn * targetntarget 个状态需要被转移,复杂度为 O(n∗target)O(n * target)O(ntarget)
  • 空间复杂度:O(target)O(target)O(target)


一维空间优化



事实上,我们还能继续进行空间优化:只保留代表「剩余容量」的维度,同时将容量遍历方向修改为「从大到小」。


代码:


class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        //「等和子集」的和必然是总和的一半
        int sum = 0;
        for (int i : nums) sum += i;
        int target = sum / 2;
        // 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
        if (target * 2 != sum) return false;
        // 将「物品维度」取消
        int[] f = new int[target + 1];
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            // 将「容量维度」改成从大到小遍历
            for (int j = target; j >= 0; j--) {
                // 不选第 i 件物品
                int no = f[j];
                // 选第 i 件物品
                int yes = j >= t ? f[j-t] + t : 0;
                f[j] = Math.max(no, yes);
            }
        }
        // 如果最大价值等于 target,说明可以拆分成两个「等和子集」
        return f[target] == target;
    }
}
复制代码


  • 时间复杂度:targettargettarget 为数组总和的一半,nnn 数组元素个数。为共有 n∗targetn * targetntarget 个状态需要被转移,复杂度为 O(n∗target)O(n * target)O(ntarget)
  • 空间复杂度:O(target)O(target)O(target)


总结



今天我们对昨天学的「01 背包」进行了应用。


可以发现,本题的难点在于对问题的抽象,主要考察的是如何将原问题转换为一个「01 背包」问题。


事实上,无论是 DP 还是图论,对于特定问题,大多都有相应的模型或算法。


难是难在如何将问题转化为我们的模型。


至于如何培养自己的「问题抽象能力」?


首先通常需要我们积累一定的刷题量,并对「转换问题的关键点」做总结。


例如本题,一个转换「01 背包问题」的关键点是我们需要将「划分等和子集」的问题等效于「在某个数组中选若干个数,使得其总和为某个特定值」的问题。


拓展



但这道题到这里还有一个“小问题”。


就是我们最后是通过「判断」来取得答案的。


通过判断取得的最大价值是否等于 targettargettarget 来决定是否能划分出「等和子集」。


虽然说逻辑上完全成立,但总给我们一种「间接求解」的感觉。


造成这种「间接求解」的感觉,主要是因为我们没有对「01 背包」的「状态定义」和「初始化」做任何改动。


但事实上,我们是可以利用「01 背包」的思想进行「直接求解」的。


因此在下一讲,我们还会再做一遍这道题。


不过却是以「另外一个角度」的「01 背包」思维来解决。


敬请期待 ~


背包问题(目录)

  1. 01背包 : 背包问题 第一讲
  1. 【练习】01背包 : 本篇
  2. 【学习&练习】01背包 : 背包问题 第三讲
  1. 完全背包 : 背包问题 第四讲
  1. 【练习】完全背包 : 背包问题 第五讲
  2. 【练习】完全背包 : 背包问题 第六讲
  3. 【练习】完全背包 : 背包问题 第七讲
  1. 多重背包 : 背包问题 第八讲
  2. 多重背包(优化篇)
  1. 【上】多重背包(优化篇): 背包问题 第九讲
  2. 【下】多重背包(优化篇): 背包问题 第十讲
  1. 混合背包 : 背包问题 第十一讲
  2. 分组背包 : 背包问题 第十二讲
  1. 【练习】分组背包 : 背包问题 第十三讲
  1. 多维背包
  1. 【练习】多维背包 : 背包问题 第十四讲
  2. 【练习】多维背包 : 背包问题 第十五讲
  1. 树形背包 : 背包问题 第十六讲
  1. 【练习篇】树形背包 : 背包问题 第十七讲
  2. 【练习篇】树形背包
  1. 背包求方案数
  1. 【练习】背包求方案数
  1. 背包求具体方案
  1. 【练习】背包求具体方案
  1. 泛化背包
  1. 【练习】泛化背包
相关文章
|
数据安全/隐私保护 网络架构
DSL线路如何工作?
【4月更文挑战第15天】
479 3
DSL线路如何工作?
|
测试技术
【动态规划刷题 10】最大子数组和 III && 环形子数组的最大和
【动态规划刷题 10】最大子数组和 III && 环形子数组的最大和
面向服务架构(SOA)吐血整理
面向服务架构(SOA)吐血整理
面向服务架构(SOA)吐血整理
|
机器学习/深度学习 自然语言处理 分布式计算
知识图谱(Knowledge Graph)之综述理解
知识图谱(Knowledge Graph)之综述理解
1313 0
知识图谱(Knowledge Graph)之综述理解
|
3月前
|
人工智能 移动开发 物联网
ModelScope魔搭25年6月发布月报
从2022年11月的青涩发布,魔搭现今已进入第三个年头,成为中国最大最活跃的开源模型社区,与超过1600万的开发者同行。
215 6
|
算法 搜索推荐 Java
解析01背包问题及其在动态规划中的应用
解析01背包问题及其在动态规划中的应用
|
机器学习/深度学习 人工智能 编解码
2024通义语音AI技术图景,大模型引领AI再进化(3)
2024通义语音AI技术图景,大模型引领AI再进化
|
机器学习/深度学习 人工智能 自然语言处理
AI基础科普:揭开人工智能的神秘面纱
人工智能(Artificial Intelligence, AI)已经成为现代科技的热门话题,影响着我们的生活方方面面。从语音助手到自动驾驶汽车,AI正在以惊人的速度改变着世界。然而,对于许多人来说,AI仍然是一个模糊的概念。本文将通过通俗易懂的语言和丰富的图文,全面介绍AI的基础知识,帮助读者更好地理解这个激动人心的领域。
|
存储 算法 安全
加密解密AES(证件号、手机号)
加密解密AES(证件号、手机号)
|
机器学习/深度学习 分布式计算 监控
大模型开发:你如何使用大数据进行模型训练?
在大数据模型训练中,关键步骤包括数据准备(收集、清洗、特征工程、划分),硬件准备(分布式计算、并行训练),模型选择与配置,训练与优化,监控评估,以及模型的持久化与部署。过程中要关注数据隐私、安全及法规遵循,利用技术进步提升效率和性能。
2038 2