详解完全背包一维空间优化推导(附背包问题攻略)

简介: 详解完全背包一维空间优化推导(附背包问题攻略)

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


题目描述



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


Tag : 「完全背包」、「动态规划」、「背包问题」


给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。


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


完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。


示例 1:


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


示例 2:


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


提示:


  • 1 <= n <= 10^4104


完全背包(朴素解法)



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


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


这一步其实就是把所有可能用到的「物品」预处理出来。


从而将问题转换为:给定了若干个数字,每个数字可以被使用无限次,求凑出目标值 nn 所需要用到的是最少数字个数是多少。


由于题目没有限制我们相同的「完全平方数」只能使用一次,属于「完全背包」模型。


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


  • 第一维 ii 代表物品编号
  • 第二维 jj 代表容量


其中第二维 jj 又有「不超过容量 jj」和「容量恰好为 jj」两种定义,本题要我们求「恰好」凑出 nn 所需要的最少个数。


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


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


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


  • 00 个数字 ii,此时有 f[i][j] = f[i - 1][j]f[i][j]=f[i1][j]
  • 11 个数字 ii,此时有 f[i][j] = f[i - 1][j - t] + 1f[i][j]=f[i1][jt]+1
  • 22 个数字 ii,此时有 f[i][j] = f[i - 1][j - 2 * t] + 2f[i][j]=f[i1][j2t]+2 ...
  • kk 个数字 ii,此时有 f[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 \leqslant k * t \leqslant jf[i][j]=min(f[i1][jkt]+k),0ktj


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


代码(朴素完全背包问题的复杂度是 O(n^2 * \sqrt{n})O(n2n) 的,有超时风险,让物品下标从 00 开始,单独处理第一个物品的 P2P2 代码勉强能过):


class Solution {
    int INF = 0x3f3f3f3f;
    public int numSquares(int n) {
        // 预处理出所有可能用到的「完全平方数」
        List<Integer> list = new ArrayList<>();
        int t = 1;
        while (t * t <= n) {
            list.add(t * t);
            t++;
        }
        // f[i][j] 代表考虑前 i 个物品,凑出 j 所使用到的最小元素个数
        int m = list.size();
        int[][] f = new int[m + 1][n + 1]; 
        // 当没有任何数时,除了 f[0][0] 为 0(花费 0 个数值凑出 0),其他均为无效值
        Arrays.fill(f[0], INF);
        f[0][0] = 0; 
        // 处理剩余数的情况
        for (int i = 1; i <= m ; i++) {
            int x = list.get(i - 1);
            for (int j = 0; j <= n; j++) {
                // 对于不选第 i 个数的情况
                f[i][j] = f[i - 1][j];
                // 对于选 k 次第 i 个数的情况
                for (int k = 1; k * x <= j; k++) {
                    // 能够选择 k 个 x 的前提是剩余的数字 j - k * x 也能被凑出
                    if (f[i - 1][j - k * x] != INF) {
                        f[i][j] = Math.min(f[i][j], f[i - 1][j - k * x] + k);
                    }
                }
            }
        }
        return f[m][n];
    }
}
复制代码


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 也能被凑出
                    // 使用 0x3f3f3f3f 作为最大值(预留累加空间)可以省去该判断
                    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})O(n),共有 n * \sqrt{n}nn 个状态需要转移,每个状态转移最多遍历 nn 次,因此转移完所有状态复杂度为 O(n^2 * \sqrt{n})O(n2n)。整体复杂度为 O(n^2 * \sqrt{n})O(n2n)
  • 空间复杂度:O(n * \sqrt{n})O(nn)


完全背包(进阶)



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


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


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


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


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


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


同时,预处理「物品」的逻辑也能直接下放到转移过程去做。


代码:


class Solution {
    public int numSquares(int n) {
        int[] f = new int[n + 1];
        Arrays.fill(f, 0x3f3f3f3f);
        f[0] = 0;
        for (int t = 1; t * t <= n; t++) {
            int x = t * t;
            for (int j = x; j <= n; j++) {
                f[j] = Math.min(f[j], f[j - x] + 1);
            }
        }
        return f[n];
    }
}
复制代码


  • 时间复杂度:共有 n * \sqrt{n}nn 个状态需要转移,复杂度为 O(n * \sqrt{n})O(nn)
  • 空间复杂度:O(n)O(n)


背包问题(目录)



  1. 01背包 :背包问题 第一讲
  1. 【练习】01背包 : 背包问题 第二讲(416. 分割等和子集)
  2. 【学习&练习】01背包 : 背包问题 第三讲(416. 分割等和子集)
  1. 完全背包 :背包问题 第四讲
  1. 【练习】完全背包 : 背包问题 第五讲(279. 完全平方数)
  2. 【练习】完全背包 : 背包问题 第六讲(322. 零钱兑换)
  3. 【练习】完全背包 : 背包问题 第七讲(518. 零钱兑换 II)
  1. 多重背包 : 背包问题 第八讲
  2. 多重背包(优化篇)
  1. 多重背包(优化篇): 背包问题 第九讲
  2. 多重背包(优化篇): 背包问题 第十讲
  1. 混合背包
  1. 【练习】混合背包
  1. 分组背包
  1. 【练习】分组背包
  1. 多维背包
  1. 【练习】多维背包 : 背包问题 第 * 讲(474. 一和零)
  2. 【练习】多维背包 : 背包问题 第 * 讲(879. 盈利计划)
  1. 树形背包
  1. 【练习】树形背包
  1. 背包求方案数
  1. 【练习】背包求方案数 : 背包问题 第 * 讲(494. 目标和)
  2. 【练习】背包求方案数 : 背包问题 第 * 讲(879. 盈利计划)
  1. 背包求具体方案
  1. 【练习】背包求具体方案 : 背包问题 第 * 讲(1049. 最后一块石头的重量 II)
  1. 泛化背包
  1. 【练习】泛化背包


最后



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


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


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


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

相关文章
|
2月前
|
算法
动态规划算法学习三:0-1背包问题
这篇文章是关于0-1背包问题的动态规划算法详解,包括问题描述、解决步骤、最优子结构性质、状态表示和递推方程、算法设计与分析、计算最优值、算法实现以及对算法缺点的思考。
83 2
动态规划算法学习三:0-1背包问题
|
2月前
|
存储 算法
算法之背包问题
本文讨论了可分背包问题和0-1背包问题的区别及解决方法,其中可分背包问题可以使用贪心算法解决,而0-1背包问题则通常采用动态规划方法来找到最大价值的解决方案。
43 0
算法之背包问题
|
算法 Python
贪心算法-分数背包问题(Python实现)
贪心算法-分数背包问题(Python实现)
117 0
|
7月前
【编程题-错题集】分割等和子集(动态规划 - 01背包)
【编程题-错题集】分割等和子集(动态规划 - 01背包)
|
7月前
装箱问题(背包问题)
装箱问题(背包问题)
64 0
|
算法
【贪心法】分数背包问题
【贪心法】分数背包问题
301 0
|
机器学习/深度学习 算法 计算机视觉
【背包问题】基于禁忌搜索算法求解背包问题附Matlab代码
【背包问题】基于禁忌搜索算法求解背包问题附Matlab代码
洛谷P2430-严酷的训练(一维01背包)
洛谷P2430-严酷的训练(一维01背包)
洛谷P2430-严酷的训练(一维01背包)
|
算法 Java 决策智能
【算法模板】动态规划(基础背包篇)—附习题(一)
【算法模板】动态规划(基础背包篇)—附习题(一)
158 0
【算法模板】动态规划(基础背包篇)—附习题(一)