使用「换元一维优化」方式求解完全背包|Java 刷题打卡

简介: 使用「换元一维优化」方式求解完全背包|Java 刷题打卡

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


题目描述



这是 LeetCode 上的518. 零钱兑换 II,难度为 Medium


给定不同面额的硬币和一个总金额。


写出函数来计算可以凑成总金额的硬币组合数。


假设每一种面额的硬币有无限个。


示例 1:


输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
复制代码


示例 2:


输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
复制代码


示例 3:


输入: amount = 10, coins = [10] 
输出: 1
复制代码


注意:


你可以假设:


  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数


完全背包(朴素解法)



在上一题 [322. 零钱兑换] 中,我们求的是「取得特定价值所需要的最小物品个数」。


对于本题,我们求的是「取得特定价值的方案数量」。


求的东西不一样,但问题的本质没有发生改变,同样属于「组合优化」问题。


你可以这样来理解什么是「组合问题」:


被选物品之间不需要满足特定关系,只需要选择物品,以达到「全局最优」或者「特定状态」即可。


同时硬币相当于我们的物品,每种硬币可以选择「无限次」,很自然的想到「完全背包」。


这时候可以将「完全背包」的状态定义搬过来进行“微调”:


定义 f[i][j]f[i][j] 为考虑前 ii 件物品,凑成总和为 jj 的方案数量。


为了方便初始化,我们一般让 f[0][x]f[0][x] 代表不考虑任何物品的情况。


因此我们有显而易见的初始化条件:f[0][0] = 1f[0][0]=1,其余 f[0][x] = 0f[0][x]=0


代表当没有任何硬币的时候,存在凑成总和为 0 的方案数量为 1;凑成其他总和的方案不存在。


当「状态定义」与「基本初始化」有了之后,我们不失一般性的考虑 f[i][j]f[i][j] 该如何转移。


对于第 ii 个硬币我们有两种决策方案:


  • 不使用该硬币:

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

  • 使用该硬币:由于每个硬币可以被选择多次(容量允许的情况下),因此方案数量应当是选择「任意个」该硬币的方案总和:


f[i][j] = \sum_{k = 1}^{\left \lfloor {j / val} \right \rfloor}f[i - 1][j - k * val], val = nums[i - 1]f[i][j]=k=1j/valf[i1][jkval],val=nums[i1]


代码:


class Solution {
    public int change(int cnt, int[] cs) {
        int n = cs.length;
        int[][] f = new int[n + 1][cnt + 1];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = 0; j <= cnt; j++) {
                f[i][j] = f[i - 1][j];
                for (int k = 1; k * val <= j; k++) {
                    f[i][j] += f[i - 1][j - k * val];  
                }
            }
        }
        return f[n][cnt];
    }
}
复制代码


  • 时间复杂度:共有 n * cntncnt 个状态需要转移,每个状态转移最多遍历 cntcnt 次。整体复杂度为 O(n * cnt^2)O(ncnt2)
  • 空间复杂度:O(n * cnt)O(ncnt)


完全背包(一维优化)



显然二维完全背包求解方案复杂度有点高。


nn 的数据范围为 10^2102cntcnt 的数据范围为 10^3103,总的计算量为 10^8108 以上,处于超时边缘(实际测试可通过)。


我们需要对其进行「降维优化」,可以使用最开始讲的 数学分析方式,或者上一讲讲的 换元优化方式 进行降维优化。


由于 数学分析方式 十分耗时,我们用得更多的 换元优化方式。两者同样具有「可推广」特性。


因为后者更为常用,所以我们再来回顾一下如何进行 换元一维优化


  1. 在二维解法的基础上,直接取消「物品维度」
  2. 确保「容量维度」的遍历顺序为「从小到大」(适用于「完全背包」)
  3. 将形如 f[i - 1][j - k * val]f[i1][jkval] 的式子更替为 f[j - val]f[jval],同时解决「数组越界」问题(将物品维度的遍历修改为从 valval 开始)


代码:


class Solution {
    public int change(int cnt, int[] cs) {
        int n = cs.length;
        int[] f = new int[cnt + 1];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = val; j <= cnt; j++) {
                f[j] += f[j - val];
            }
        }
        return f[cnt];
    }
}
复制代码


  • 时间复杂度:共有 n * cntncnt 个状态需要转移,整体复杂度为 O(n * cnt)O(ncnt)
  • 空间复杂度:O(cnt)O(cnt)


总结



[322. 零钱兑换] 和 本篇的「518. 零钱兑换 II」本质是一样的。


之所将两题分开成两篇【练习】,主要是为了加强大家对于「一维优化」的熟练度。


上来先写一个「二维朴素版」然后再进行「数学分析」推导这样的做法太慢了,不适合于比赛或者笔试情景。


我们应当做到:上手就能写出「一维优化」版本,但同时在脑中思考的是二维的递推逻辑 ~


最后



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


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


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


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

相关文章
|
6天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
22 6
|
16天前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
23 4
|
14天前
|
存储 Java 开发者
成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享
本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。
|
19天前
|
缓存 前端开发 JavaScript
9大高性能优化经验总结,Java高级岗必备技能,强烈建议收藏
关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。本文介绍了9种性能优化方法,涵盖代码优化、数据库优化、连接池调优、架构层面优化、分布式缓存、异步化、Web前端优化、服务化、硬件升级、搜索引擎和产品逻辑优化。欢迎留言交流。
|
19天前
|
存储 缓存 Java
Java应用瘦身记:Docker镜像从674MB优化至58MB的实践指南
【10月更文挑战第22天】 在容器化时代,Docker镜像的大小直接影响到应用的部署速度和运行效率。一个轻量级的Docker镜像可以减少存储成本、加快启动时间,并提高资源利用率。本文将分享如何将一个Java基础Docker镜像从674MB缩减到58MB的实践经验。
29 1
|
20天前
|
消息中间件 监控 算法
Java性能优化:策略与实践
【10月更文挑战第21】Java性能优化:策略与实践
|
19天前
|
SQL 监控 Java
Java性能优化:提升应用效率与响应速度的全面指南
【10月更文挑战第21】Java性能优化:提升应用效率与响应速度的全面指南
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
3天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
16 9
|
6天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####