当前位置: 首页 > news >正文

力扣每日一刷Day 20

Practice Day twenty:Leetcode T 322

今天,我们来讲讲背包DP

背包DP(Knapsack Dynamic Programming)

一种经典的动态规划问题,主要解决在有限容量下如何选择物品使得总价值最大。最常见的是0-1背包问题

通常我们会通过遍历的方式穷举所有可能,才能得到最大值,比较好的实现是表格法。

这就是背包DP的所有讲解了。

现在,我们来看看今天的Leetcode

题目条件:无限硬币、硬币面额、目标金额

题目目的:最少硬币达到目标金额

今天我们选用的是力扣不知名老哥的代码

老哥的代码在速度上登峰造极,来到了0ms,空间复杂度也超过96%的人

时间复杂度:O(N∗Amount)

空间复杂度:O(Amount)

先给你们看看这位老哥代码的全貌,事实上我第一眼我就感觉超纲了。不过这并不妨碍我们学新知识。

const auto _ = std::cin.tie(nullptr)->sync_with_stdio(false);
#define LC_HACK
#ifdef LC_HACK
const auto __ = []() {struct ___ {static void _() { std::ofstream("display_runtime.txt") << 0 << '\n'; }};std::atexit(&___::_);return 2;
}();
#endifclass Solution {
public:int coinChange(vector<int>& coins, int amount) {constexpr int max_coins{ 10001 };int dp[max_coins]{ 0 }; fill(dp + 1, dp + amount + 1, max_coins);for (size_t i=0; i < coins.size(); ++i){for (size_t j{ static_cast<size_t>(coins[i]) }; j < amount + 1; ++j){dp[j] = min(dp[j], dp[j - coins[i]] + 1);}}return dp[amount] == max_coins ? -1 : dp[amount];}
};

现在我们来开始着手构建他吧!

1 代码一开始就关闭了同步,这种做法在我们Day 13就已经见过,并详细解释其作用了,但这里还多了一些东西,即tie函数我们需要详细讲解

const auto _ = std::cin.tie(nullptr)->sync_with_stdio(false);
  1. std::cin.tie(nullptr)(这可能会带来一些危害,好奇的小伙伴可以自己查询一下)

    • 默认情况下,std::cinstd::cout 是绑定(tie)在一起的。每次调用 std::cout 后,会自动刷新缓冲区,并且在调用 std::cin 之前也会自动刷新 std::cout 的缓冲区。

    • 使用 std::cin.tie(nullptr) 可以解除这种绑定,避免不必要的缓冲区刷新,从而提升性能。

    • tie()std::ios_base 的一个成员函数。

    • std::cin 是一个 std::istream 对象,它继承自 std::ios_base

    • tie(nullptr) 的作用是将 std::cin 解绑(解除与 std::cout 的绑定)。

    • 它返回的是一个指向 std::ios_base 类型对象的指针。

  2. ->sync_with_stdio(false)

    • C++ 的标准 I/O 流 (cin, cout) 默认与 C 标准库的 I/O 流 (stdin, stdout) 同步,以便兼容性。

    • 设置为 false 可以关闭这种同步机制,从而显著加快输入输出速度,但代价是不能混用 cin/coutscanf/printf

    • -> 是对前面返回的指针使用成员访问运算符;
    • sync_with_stdio(false) 是调用该指针所指向对象的 sync_with_stdio 成员函数,并传入参数 false
    • 这个函数的作用是关闭 C++ I/O 流和 C 标准 I/O 流之间的同步,从而提高性能。
  3. const auto _ = ...

    将返回值赋给一个临时变量 _,确保这条语句不会被编译器优化掉(因为某些编译器可能会忽略没有副作用的语句)。

2 然后开启条件编译控制。不过老实说,我也不太懂为什么要这么做

#define LC_HACK   /*定义一个宏LC_HACK*/
#ifdef LC_HACK   /*如果LC_HACK被定义,那就开始执行下面的操作*/

这是我下面找到的可能的原因。事实上在看完之后,我仍然认为这个没有必要

1. 区分 “调试 / 开发” 与 “生产 / 发布” 版本

开发阶段可能需要打印调试日志、验证内存泄漏的代码,但发布时这些代码会影响性能或暴露敏感信息。

#define DEBUG  // 开发时定义,发布时注释掉
#ifdef DEBUG// 调试代码:打印变量值、执行额外校验cout << "调试:当前变量值=" << x << endl;
#endif

如果直接编译,调试代码会被保留,而条件编译可以一键 “剔除”。

2. 适配不同平台或环境

同一套代码可能需要在 Windows、Linux、移动端等不同平台运行,而不同平台的底层接口可能不同。
例如:

#ifdef _WIN32  // Windows平台宏// Windows特有的文件操作代码
#elif __linux__  // Linux平台宏// Linux特有的文件操作代码
#endif

直接编译会导致某一平台的代码在另一平台报错,条件编译则能针对性地选择适配代码。

3. 临时启用 / 禁用功能模块

开发中可能需要临时测试某个功能(如代码中的LC_HACK对应的 “程序退出时写文件” 功能),但正式版本不需要。
如果直接编译,要么保留冗余代码,要么手动删除(后续可能还要恢复),而条件编译只需注释 / 取消注释#define LC_HACK即可切换,更灵活且不易出错。

4. 避免重复定义

头文件中常用#ifndef防止重复包含(即 “头文件保护”):

#ifndef MY_HEADER_H
#define MY_HEADER_H// 头文件内容(类、函数声明等)
#endif

好的,条件编译的使用条件大家已经有所了解,我们继续回到老哥的代码解析

我们回忆一下, 由于宏被定义,所以执行以下操作

3 然后弄一个程序一启动,就立即执行的匿名lambda函数。关于lambda,这种函数类型我们之前讲过,在Day15就已经完整的讲解过他的作用,此处不再重复讲解

const auto __ = []() {

虽然不写匿名的lambda也可以执行里面的操作,但是写lambda可以让这些操作具备封装性,并减少命名冲突,同时lambda立即执行的特性,也能让人马上知晓:这是一次性的启动初始化操作

好了,让我们看看函数里面有什么

  struct ___ {static void _() { std::ofstream("display_runtime.txt") << 0 << '\n'; }};std::atexit(&___::_);return 2;
}();

我嘞个飞天大雷,我就说为什么要弄这个东西:
这段代码的效果是:程序一旦正常结束,就会自动在当前目录生成(或覆盖)display_runtime.txt文件,并写入0

也就是说无论这个代码运行了多久,3ms也好,4ms也好,只要加上这串代码,统一显示为0!真是太见鬼了,这不就是作弊吗!?

我就说怎么他的时间复杂度和空间复杂度和官方解答一样, 怎么会快官方解答这么多!

要不是这里没有表情包,我真想放一个笑哭的表情上去
好吧,作弊就作弊吧,起码让哥几个学到真东西了,我们继续

回调函数
核心特点是被当作参数传递给另一个函数,然后在特定时机被调用执行。简单说,就是 “你定义一个函数,交给别人(其他函数或系统),让别人在需要的时候帮你调用”。
下面展示的代码就是回调函数的一个例子

___::_
/*其中___是我们在lambda中定义的结构体名称,_是结构体中定义的函数名称*/

你只是定义了一个函数_,但是没有调用它,他就不管用,所以我们需要一个函数调用它,这个函数就是atexit()

接下来,我们来讲讲atexit函数的注册。注册的意思如下

类比:

这就像你去电影院看电影,离场前在前台 “注册” 了一个需求:“请在散场时提醒我带伞”。

  • 注册:告诉前台你的需求和触发时机(散场时)
  • 触发:电影结束(对应程序退出),前台履行约定提醒你(对应调用注册的函数)
  1. std::atexit(&___::_);

    • 这是 C++ 标准库函数atexit的调用,用于注册一个 "程序退出时的回调函数"
    • &___::_表示取结构体___中静态成员函数_()的地址,作为回调函数
    • 作用是:当程序正常退出时(无论是从main函数返回、调用exit()函数,还是程序自然结束),系统会自动调用___::_()函数,执行文件写入操作。
    • 执行时机保障
      整个逻辑被包裹在 “立即执行的 lambda 表达式” 中:这确保了std::atexit的注册操作在程序启动阶段(main函数执行前) 就完成,保证回调函数在程序生命周期的最后阶段被正确调用。
  2. return 2;

    • 这是 lambda 表达式的返回语句,返回值为整数 2
    • 由于这个 lambda 表达式是立即执行的(末尾有()),其返回值会被赋值给前面的const auto __变量
    • 从代码逻辑看,这个返回值 "2" 没有实际用途,只是为了满足 lambda 表达式需要有返回值的语法要求

看见最后那个#endif了吗,还记得上面我们有#ifdef LC_HACK  ,这是开启命令的指令,而#endif就是结束执行的标志。

好了,偷天换日的手法已经被我们学会了,我们接下来学习正经东西,也就是我们真正的背包DP。

4 先给个壳子,看看传入什么参数:coins硬币面额,amount目标金额

    int coinChange(vector<int>& coins, int amount) {

5 在此,我们需要定义一个常量,作为初始化我们组合数组的初值,同时,这个初值也会成为我们判断方案是否可行的数值,这在很多涉及到数组的方法中常见。这里我们把他初始化为一个不可达值。

	constexpr int max_coins{ 10001 };

我们还没有见过constexpr,来解释一下

constexpr 关键字
表示这是一个编译期常量,意味着它的值在编译阶段就会被确定,而不是在程序运行时计算。这有助于编译器进行优化,同时确保该值在程序运行过程中不会被修改。

现在,我们来讲讲constexpr和const的区别

  1. 初始化时机

    • const int max_coins = 10001; 表示运行时常量,其值在编译期可以不确定,只要在运行时初始化后不再修改即可
    • constexpr int max_coins{10001}; 表示编译期常量,其值必须在编译阶段就确定,编译器会在编译时计算并嵌入代码中
  2. 适用场景

    • 对于数组大小、模板参数等必须在编译期确定的值,只能用constexpr
    • 在后面的代码中,虽然max_coins用于初始化数组int dp[max_coins]{0};,这里用const也能编译通过(C++ 允许const整数作为数组大小),但使用constexpr更明确地表达了 "这是一个编译期确定的常量" 的意图
  3. 优化潜力

    • constexpr常量会被编译器视为 "真正的常量",可能会在编译时直接替换到使用它的地方(类似宏替换,但更安全)
    • 对于动态规划中的边界判断dp[amount] == max_coinsconstexpr可以让编译器生成更高效的比较指令

简单说,在这个场景下,constexprconst更严格地保证了值的常量性,并且明确告诉编译器:"这个值在编译时就已知,可以放心地进行优化"。虽然用const也能工作,但constexpr是更合适的选择。

6 然后以数组索引为目标金额,数组存储的值为所用最少的硬币数目,创建数组

		int dp[max_coins]{ 0 };

然后顺便初始化0值,因为凑出0块钱只需要0枚硬币

7 现在,我们需要对数组剩余的元素进行初始化

 fill(dp + 1, dp + amount + 1, max_coins);

新面孔,让我们来看看他是怎么一回事

fill(起始地址, 结束地址, 要填充的值),把所有数值初始化为我们定义的最大值,一值两用,很合理

8 然后遍历每一种硬币面额。在此之前我们回忆了一下参数,确信是传入了coins数组的

		for (size_t i=0; i < coins.size(); ++i){

在这里我们又看到新东西了,size_t
size_t 是 C++ 中一个特殊的无符号整数类型(unsigned integer type),专门用于表示对象的大小、数组的索引或容器的元素数量

什么用 size_t 而不是 int

  • 语义更清晰:明确表示 “这是一个长度 / 索引,而非普通整数”
  • 避免负数问题:数组索引和大小不可能为负,无符号类型更安全(例如避免 i < -1 这类逻辑错误)
  • 兼容性更好:与标准库的接口(如 size() 方法,在我们后面的coins.size()有用到)完美匹配,避免类型转换警告

9 然后以当前硬币金额,遍历全部的面额,开始构造完全背包(完全背包的含义:在背包容量是定额的同时,每个物件可以使用无数次),

for (size_t j{ static_cast<size_t>(coins[i]) }; j < amount + 1; ++j){dp[j] = min(dp[j], dp[j - coins[i]] + 1);}}

好吧,一步步拆解吧,新面孔也是不少

just look at this

size_t j{ static_cast<size_t>(coins[i]) }

这行代码的作用是将 coins[i] 的值转换为 size_t 类型,并赋值给变量 j。这里的关键点包括:

  1. coins[i]:硬币的面额(例如 [1, 2, 5]),而 i 是当前访问的索引。

  2. static_cast<size_t>(...):使用静态类型转换,将 coins[i] 的值显式转换为无符号整数类型 size_t。通常用于确保操作在处理容器大小时不会出现类型不匹配的问题。

  3. j:最终得到的是与 coins[i] 值相等的无符号整数。

然后继续看看第2条是怎么回事

static_cast<目标类型>(表达式)

说明:

  • static_cast:是 C++ 提供的一种安全、静态检查的类型转换方式。

  • 目标类型:是你希望将表达式转换成的类型,例如 intdoublesize_t 等。

  • 表达式:是要被转换的值或变量。

然后就是说明一下,为什么要用{ }来给 进行初始化了。这实际上是一种列表初始化

列表初始化有诸如下面的好处

核心特点

  1. 禁止窄化转换(最关键特性)
    不允许可能导致数据丢失的隐式转换,编译时会报错:

    int x{3.14};  // 错误!double转int会丢失小数部分(窄化转换)
    char c{1000}; // 错误!1000超出char的取值范围(通常-128~127)

    这比传统的=初始化更安全,能避免意外的数据截断。

  2. 无歧义
    解决了 C++ 中传统()初始化的 "最令人头疼的解析" 问题:

    // 传统方式可能被解析为函数声明
    int func();  // 函数声明,返回int// 列表初始化明确为对象初始化
    int func{10}; // 变量初始化,值为10
  3. 统一语法
    无论初始化变量、数组、容器还是自定义对象,都可以使用{},风格一致,降低学习成本。

接着,我们来看看状态转移方程

				dp[j] = min(dp[j], dp[j - coins[i]] + 1);
  1. dp[j]的含义
    dp[j]表示在当前计算状态下,组成金额j所需要的最少硬币数量。在执行这行代码前,它可能已经存储了不使用当前硬币时的最优解。

  2. dp[j - coins[i]] + 1的含义

    • coins[i]是当前正在处理的硬币面额
    • j - coins[i]表示使用 1 枚当前硬币后剩余的金额
    • dp[j - coins[i]]是组成这个剩余金额所需的最少硬币数
    • +1表示加上当前使用的这 1 枚硬币
  3. min函数的作用
    比较两种方案的结果并取当前面额 j 更少硬币的组合

    • 不使用当前硬币的方案(dp[j]
    • 使用 1 枚当前硬币的方案(dp[j - coins[i]] + 1
      最终将更优的结果(更少的硬币数量)赋值给dp[j]

但是这就会出现另一个问题:j - coins[i]这个索引他会小于0吗?

答案是:不会

这是因为我们初始的 被设置为了当前处理的硬币面额,所以 j - coins[i]最小就是0!不会出现小于0的情况!

9 当我们所有循环结束后,我们返回答案。

		return dp[amount] == max_coins ? -1 : dp[amount];

这也没什么好说的了,如果没有找到best match,就返回一个-1,如果找到了,就返回找到的值。

that‘s all,感谢不知名老哥提供的代码


文章转载自:

http://xnHnUQ5E.rnrwq.cn
http://woU6EdQa.rnrwq.cn
http://YO3tX3uJ.rnrwq.cn
http://iTofEijU.rnrwq.cn
http://9i3ltI92.rnrwq.cn
http://2Cy5dVS0.rnrwq.cn
http://7ckpnGWa.rnrwq.cn
http://ea0Fu3jn.rnrwq.cn
http://JvVfGgrv.rnrwq.cn
http://MrLsL6iD.rnrwq.cn
http://Bsus50j7.rnrwq.cn
http://rk8CHBrM.rnrwq.cn
http://rkqBonbW.rnrwq.cn
http://Dj2jNn2A.rnrwq.cn
http://b6orL1ME.rnrwq.cn
http://fkFhODPE.rnrwq.cn
http://WXpkhE0L.rnrwq.cn
http://1eHmUTXY.rnrwq.cn
http://dNhp3Gda.rnrwq.cn
http://riOmqgWC.rnrwq.cn
http://1HIfaShv.rnrwq.cn
http://OQxT0Un6.rnrwq.cn
http://j3jee0SZ.rnrwq.cn
http://g0szhBag.rnrwq.cn
http://Vc1OXQhX.rnrwq.cn
http://knFWyf1i.rnrwq.cn
http://4GoLEpoP.rnrwq.cn
http://3tV90Mqz.rnrwq.cn
http://mNcm9G05.rnrwq.cn
http://PKYbBwOs.rnrwq.cn
http://www.dtcms.com/a/376627.html

相关文章:

  • 线程池队列与活跃度报警检测器实现详解
  • 【硬件-笔试面试题-80】硬件/电子工程师,笔试面试题(知识点:MOS管与三极管的区别)
  • A股大盘数据-20250910分析
  • 大数据毕业设计-基于大数据的健康饮食推荐数据分析与可视化系统(高分计算机毕业设计选题·定制开发·真正大数据)
  • 墨水屏程序
  • 小米自带浏览器提示“已停止访问该网页”的解决办法以及一些优化
  • 零代码入侵:Kubernetes 部署时自动注入 kube-system UID 到 .NET 9 环境变量
  • Python核心技术开发指南(049)——文件操作综合应用
  • 机器学习项目中正确进行超参数优化:Optuna库的使用
  • QueryWrapper 全面解析:从原理到实战
  • 2025时序数据库选型:深入解析IoTDB从主从架构基因到AI赋能的创新之路
  • 云手机可以用来托管游戏吗?
  • 每日算法之:给定一个有序数组arr,代表坐落在X轴上的点,给定一个正数K,代表绳子的长度,返回绳子最多压中几个点? 即使绳子边缘处盖住点也算盖住
  • 如何利用AI工具更好地服务人:从效率到温度的平衡
  • ADC模数转换器详解(基于STM32)
  • 深入理解网络浏览器运行原理
  • 线扫相机不出图原因总结
  • 【Linux系统】日志与策略模式
  • 物联网时序数据库IoTDB是什么?
  • Rust:系统编程的革新者
  • 【postMan / apifox 文件上传】
  • 使用 javax.net.ssl.HttpsURLConnection 发送 HTTP 请求_以及为了JWT通信选用OSS的Jar的【坑】
  • 9.10 Swiper-layer-laydate
  • 基于代理模式:深入了解静态代理和动态代理
  • 崔传波教授:以科技与人文之光,点亮近视患者的清晰视界‌
  • java 代理模式实现
  • 2025最新的软件测试面试八股文(800+道题)
  • 深入浅出LVS负载均衡群集:原理、分类与NAT模式实战部署
  • Nginx 配置 SSL/TLS 全指南:从安装到安全强化
  • 整体设计 之 绪 思维导图引擎 之 引 认知系统 之8 之 序 认知元架构 之4 统筹:范畴/分类/目录/条目 之2 (豆包助手 之6)