前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【动态规划/背包问题】强化利用「等差」特性推导「完全背包」的核心要素

【动态规划/背包问题】强化利用「等差」特性推导「完全背包」的核心要素

作者头像
宫水三叶的刷题日记
发布2021-04-26 15:41:25
5570
发布2021-04-26 15:41:25
举报

前言

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

从本篇开始,我们会完成三道与 完全背包 相关的练习题。会进入比较轻松的「完全背包」复习强化阶段 ~

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

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

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

题目描述

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

给定正整数

n

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

n

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

给你一个整数

n

,返回和为

n

的完全平方数的「最少数量」。

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

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

示例 1:

代码语言:javascript
复制
输入:n = 12

输出:3 

解释:12 = 4 + 4 + 4

示例 2:

代码语言:javascript
复制
输入:n = 13

输出:2

解释:13 = 4 + 9

提示:

  • 1 <= n <=
10^4

完全背包(朴素解法)

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

因此我们第一步可以将范围在

[1, n]

内的「完全平方数」预处理出来。

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

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

因此我们的问题转换为:

给定了若干个数字,每个数字可以被使用无限次,求凑出目标值

n

所需要用到的是最少数字个数是多少。

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

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

  • 第一维
i

代表物品编号

  • 第二维
j

代表容量

其中第二维

j

又有「不超过容量

j

」和「容量恰好为

j

」两种定义。

本题要我们求「恰好」凑出

n

所需要的最少个数。

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

f[i][j]

为考虑前

i

个数字,凑出数字总和

j

所需要用到的最少数字数量。

不失一般性的分析

f[i][j]

,对于第

i

个数字(假设数值为

t

),我们有如下选择:

  • 选 0 个数字
i

,此时有

f[i][j] = f[i - 1][j]
  • 选 1 个数字
i

,此时有

f[i][j] = f[i - 1][j - t] + 1
  • 选 2 个数字
i

,此时有

f[i][j] = f[i - 1][j - 2 * t] + 2

...

  • 选 k 个数字
i

,此时有

f[i][j] = f[i - 1][j - k * t] + k

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

f[i][j] = min(f[i-1][j-k*t]+k),0 \leqslant k * t \leqslant j

当然,能够选择

k

个数字

i

的前提是,剩余的数字

j - k * t

也能够被其他「完全平方数」凑出,即

f[i - 1][j - k * t]

为有意义的值。

代码:

代码语言:javascript
复制
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(\sqrt{n})

,共有

n * \sqrt{n}

个状态需要转移,每个状态转移最多遍历

n

次,因此转移完所有状态复杂度为

O(n^2 * \sqrt{n})

。整体复杂度为

O(n^2 * \sqrt{n})

  • 空间复杂度:
O(n * \sqrt{n})

完全背包(进阶)

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

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

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

从二维的状态转移方程入手进行分析(假设第

i

个数字为

t

):

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

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

代码:

代码语言:javascript
复制
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(\sqrt{n})

,共有

n * \sqrt{n}

个状态需要转移,复杂度为

O(n * \sqrt{n})

。整体复杂度为

O(n * \sqrt{n})

  • 空间复杂度:
O(n)

总结

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

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

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

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

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

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

背包问题(目录)

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

最后

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

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

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

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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 题目描述
  • 完全背包(朴素解法)
  • 完全背包(进阶)
  • 总结
  • 背包问题(目录)
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档