【背包问题の第五讲】强化利用「等差」特性推导「完全背包」的核心要素

简介: 【背包问题の第五讲】强化利用「等差」特性推导「完全背包」的核心要素

前言



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


从本篇开始,我们会完成三道与 完全背包 相关的练习题,希望大家能够坚持住。


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


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


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


题目描述



这是 LeetCode 上的279. 完全平方数,难度为 Medium


给定正整数 nnn,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 nnn


你需要让组成和的完全平方数的个数最少。


给你一个整数 nnn ,返回和为 nnn 的完全平方数的「最少数量」。


「完全平方数」是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。


例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。


示例 1:


输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4
复制代码


示例 2:


输入:n = 13
输出:2
解释:13 = 4 + 9
复制代码


提示:


  • 1 <= n <= 10410^4104


完全背包(朴素解法)



首先「完全平方数」有无限个,但我们要凑成的数字是给定的。


因此我们第一步可以将范围在 [1,n][1, n][1,n] 内的「完全平方数」预处理出来。


这一步其实就是把所有可能用到的数字先预处理出来。


同时由于题目没有限制我们相同的「完全平方数」只能使用一次。


因此我们的问题转换为:


给定了若干个数字,每个数字可以被使用无限次,求凑出目标值 nnn 所需要用到的是最少数字个数是多少。


这显然符合「完全背包」模型。


目前我们学过的两类背包问题(01 背包 & 完全背包)的原始状态定义都是两维:


  • 第一维 iii 代表物品编号
  • 第二维 jjj 代表容量


其中第二维 jjj 又有「不超过容量 jjj」和「容量恰好为 jjj」两种定义。


本题要我们求「恰好」凑出 nnn 所需要的最少个数。



因此我们可以调整我们的「状态定义」:


f[i][j]f[i][j]f[i][j] 为考虑前 iii 个数字,凑出数字总和 jjj 所需要用到的最少数字数量。


不失一般性的分析 f[i][j]f[i][j]f[i][j],对于第 iii 个数字(假设数值为 ttt),我们有如下选择:


  • 选 0 个数字 iii,此时有 f[i][j]=f[i−1][j]f[i][j] = f[i - 1][j]f[i][j]=f[i1][j]
  • 选 1 个数字 iii,此时有 f[i][j]=f[i−1][j−t]+1f[i][j] = f[i - 1][j - t] + 1f[i][j]=f[i1][jt]+1
  • 选 2 个数字 iii,此时有 f[i][j]=f[i−1][j−2∗t]+2f[i][j] = f[i - 1][j - 2 * t] + 2f[i][j]=f[i1][j2t]+2

...

  • 选 k 个数字 iii,此时有 f[i][j]=f[i−1][j−k∗t]+kf[i][j] = f[i - 1][j - k * t] + kf[i][j]=f[i1][jkt]+k


因此我们的状态转移方程为:


f[i][j]=min(f[i−1][j−k∗t]+k),0⩽k∗t⩽jf[i][j] = min(f[i-1][j-k*t]+k),0 \leqslant k * t \leqslant jf[i][j]=min(f[i1][jkt]+k),0ktj


当然,能够选择 kkk 个数字 iii 的前提是,剩余的数字 j−k∗tj - k * tjkt 也能够被其他「完全平方数」凑出,即 f[i−1][j−k∗t]f[i - 1][j - k * t]f[i1][jkt] 为有意义的值。


代码:


class Solution {
    int INF = -1;
    public int numSquares(int n) {
        // 预处理出所有可能用到的「完全平方数」
        List<Integer> list = new ArrayList<>();
        int idx = 1;
        while (idx * idx <= n) {
            list.add(idx * idx);
            idx++;
        }
        // f[i][j] 代表考虑前 i 个物品,凑出 j 所使用到的最小元素个数
        int len = list.size();
        int[][] f = new int[len][n + 1]; 
        // 处理第一个数的情况
        for (int j = 0; j <= n; j++) {
            int t = list.get(0);
            int k = j / t;
            if (k * t == j) { // 只有容量为第一个数的整数倍的才能凑出
                f[0][j] = k; 
            } else { // 其余则为无效值
                f[0][j] = INF;
            }
        }
        // 处理剩余数的情况
        for (int i = 1; i < len; i++) {
            int t = list.get(i);
            for (int j = 0; j <= n; j++) {
                // 对于不选第 i 个数的情况
                f[i][j] = f[i - 1][j];
                // 对于选 k 次第 i 个数的情况
                for (int k = 1; k * t <= j; k++) {
                    // 能够选择 k 个 t 的前提是剩余的数字 j - k * t 也能被凑出
                    if (f[i - 1][j - k * t] != INF) {
                        f[i][j] = Math.min(f[i][j], f[i - 1][j - k * t] + k);
                    }
                }
            }
        }
        return f[len - 1][n];
    }
}
复制代码


  • 时间复杂度:预处理出所有可能用到的数字复杂度为 O(n)O(\sqrt{n})O(n),共有 n∗nn * \sqrt{n}nn 个状态需要转移,每个状态转移最多遍历 nnn 次,因此转移完所有状态复杂度为 O(n2∗n)O(n^2 * \sqrt{n})O(n2n)。整体复杂度为 O(n2∗n)O(n^2 * \sqrt{n})O(n2n)
  • 空间复杂度:O(n∗n)O(n * \sqrt{n})O(nn)


完全背包(进阶)



显然朴素版的完全背包进行求解复杂度有点高。


在上一节讲解 完全背包 的时候,我们从「数学」角度来推导为何能够进行一维空间优化。


这次我们还是按照同样的思路再进行一次推导,加强大家对这种优化方式的理解。


从二维的状态转移方程入手进行分析(假设第 iii 个数字为 ttt):


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


至此,我们得到了最终的状态转移方程:


f[j]=min(f[j],f[j−t]+1)f[j] = min(f[j], f[j - t] + 1)f[j]=min(f[j],f[jt]+1)


代码:


class Solution {
    int INF = -1;
    public int numSquares(int n) {
        // 预处理出所有可能用到的「完全平方数」
        List<Integer> list = new ArrayList<>();
        int idx = 1;
        while (idx * idx <= n) {
            list.add(idx * idx);
            idx++;
        }
        // f[j] 代表考虑到当前物品为止,凑出 j 所使用到的最小元素个数
        int len = list.size();
        int[] f = new int[n + 1]; 
        // 处理第一个数的情况
        for (int j = 0; j <= n; j++) {
            int t = list.get(0);
            int k = j / t;
            if (k * t == j) { // 只有容量为第一个数的整数倍的才能凑出
                f[j] = k;
            } else { // 其余则为无效值
                f[j] = INF;
            }
        }
        // 处理剩余数的情况
        for (int i = 1; i < len; i++) {
            int t = list.get(i);
            for (int j = t; j <= n; j++) {
                // 当不更新 f[j] 的时候,对应了二维表示中的 f[i - 1][j]
                // 可以更新 f[j] 的前提是:剩余的 j - k * t 也能够被凑出
                // 更新 f[j] 所依赖的 f[j - t] 对应了二维表示中的 f[i - 1][j - k * t]
                if (f[j - t] != INF) f[j] = Math.min(f[j], f[j - t] + 1);
            }
        }
        return f[n];
    }
}
复制代码


  • 时间复杂度:预处理出所有可能用到的数字复杂度为 O(n)O(\sqrt{n})O(n),共有 n∗nn * \sqrt{n}nn 个状态需要转移,复杂度为 O(n∗n)O(n * \sqrt{n})O(nn)。整体复杂度为 O(n∗n)O(n * \sqrt{n})O(nn)
  • 空间复杂度:O(n)O(n)O(n)


总结



今天,我们强化了 上一节 的「完全背包」一维优化方式的遍历顺序推导过程。


又一次的带大家从朴素的二维状态定义出发,逐步推导出一维空间的状态转移方程,并从状态转移方程确定遍历顺序。


可以发现,这道题和我们传统的「完全背包」的状态定义不同:


传统的「完全背包」的状态定义是要我们求最大价值,而本题则是求取得某个价值所需要的最少元素个数。


但模型仍然是对应「每个物品可以选择无限个,每选一个物品会带来相应的价值与成本」的「完全背包」模型。


建议大家结合 上一节 的内容好好体会。


背包问题(目录)



  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. 【练习】泛化背包
相关文章
|
编译器
鸿蒙NEXT-鸿蒙三层架构搭建,嵌入HMRouter,实现便捷跳转,新手攻略。(2/3)
本文介绍在三层架构中实现模块依赖的步骤。首先在产品定制层(features)的oh-package.json5文件中导入共享包依赖,如"basic":"file:../../commons/basic"。然后在产品层(products)的配置文件中同时导入公共能力层和产品定制层的依赖,示例展示了如何添加"basic"和"my"两个依赖项。通过这些配置,三层架构的各模块之间建立了完整的依赖关系。
129 0
鸿蒙NEXT-鸿蒙三层架构搭建,嵌入HMRouter,实现便捷跳转,新手攻略。(2/3)
|
4月前
|
IDE 搜索推荐 程序员
《CodeBuddy:像哆啦A梦一样智能的编程助手》
本文介绍腾讯云代码助手CodeBuddy——智能编程伙伴,宛如哆啦A梦般的存在。它具备智能辅助、个性化学习、多场景适配等优势,支持主流IDE与多种编程语言,保护代码隐私并开源透明。通过上下文理解、实时错误检测等功能提升开发效率;根据编码风格优化建议,构建知识图谱。下载链接提供,安装后即可在IDE中使用,助你成为更高效的开发者。
410 17
《CodeBuddy:像哆啦A梦一样智能的编程助手》
|
4月前
|
存储 运维 物联网
RFID电力巡检管理应用
RFID技术在电力巡检中广泛应用,可显著提升效率、准确性和可靠性。通过为设备安装RFID标签,记录全生命周期信息,结合GIS技术优化巡检路线。巡检人员使用手持终端读取标签,自动记录到达时间与数据,减少人工错误。系统支持异常上报、数据分析和维护提醒,实现设备精细化管理。RFID技术助力电力企业降低运维成本,延长设备寿命,保障电网稳定运行。
|
10月前
|
消息中间件 存储 缓存
十万订单每秒热点数据架构优化实践深度解析
【11月更文挑战第20天】随着互联网技术的飞速发展,电子商务平台在高峰时段需要处理海量订单,这对系统的性能、稳定性和扩展性提出了极高的要求。尤其是在“双十一”、“618”等大型促销活动中,每秒需要处理数万甚至数十万笔订单,这对系统的热点数据处理能力构成了严峻挑战。本文将深入探讨如何优化架构以应对每秒十万订单级别的热点数据处理,从历史背景、功能点、业务场景、底层原理以及使用Java模拟示例等多个维度进行剖析。
280 8
|
存储 机器学习/深度学习 安全
Java基础+进阶
本文适合Java入门和复习回顾。内容覆盖JDK下载和hello world、IDEA下载安装配置、类、基本数据类型、方法、修饰符、关键字、面向对象、继承、多态、接口、异常、集合、i/o流、多线程、网络编程、Lambda表达式、接口组成更新、方法引用、函数式接口、 Stream流、 反射、模块化、XML
Java基础+进阶
|
10月前
|
Web App开发 搜索推荐 安全
|
11月前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
466 5
|
11月前
|
API 数据安全/隐私保护 UED
文档智能(Document Intelligence)与检索增强生成(Retrieval-Augmented Generation, RAG)
文档智能(Document Intelligence)与检索增强生成(Retrieval-Augmented Generation, RAG)
264 1
|
机器学习/深度学习 运维 监控
智能运维:未来IT管理的革新之路
在数字化浪潮汹涌的今天,智能运维成为企业提升竞争力的关键。本文将深入浅出地探索智能运维的核心概念、技术应用以及它如何重塑IT管理的未来。通过具体案例,我们将一窥智能运维如何实现故障预测、自动化处理和持续优化,最终引领企业走向高效、稳定、创新的未来。
229 29
|
网络协议 安全 网络安全
深入解析TURN协议的作用与重要性
【8月更文挑战第24天】
397 0