ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

贪心算法实战:从“巧克力”问题解析最优解策略与代码实现

贪心算法实战:从“巧克力”问题解析最优解策略与代码实现 1. 项目概述从“巧克力”问题看贪心算法的实战魅力最近在准备蓝桥杯或者类似算法竞赛的同学估计没少被各种“最优解”问题折磨。今天想和大家深入聊聊一个非常经典的题目——“巧克力”问题。这题表面上是关于如何分配或购买巧克力但内核是一个典型的贪心算法应用场景。我第一次遇到这题时也觉得思路有点绕但一旦理解了其背后的贪心策略就会有种豁然开朗的感觉。它不仅能帮你稳稳拿下竞赛分数更重要的是它能训练你一种“局部最优导致全局最优”的算法思维这种思维在解决资源调度、区间安排、背包问题变种时都非常有用。无论你是算法新手还是想巩固贪心思想的同学这篇心得都会从问题本质、策略证明到代码细节给你掰开揉碎了讲清楚。简单来说“巧克力”问题通常描述为我们有一定数量的钱或者积分商店里有不同价格和快乐值或满意度的巧克力每种巧克力数量无限或有限。目标是用有限的钱获取最大的总快乐值。这听起来是不是有点像“背包问题”没错但它通常更简单因为巧克力物品可以分割如果题目允许或者单位价格的价值很容易计算这恰恰是贪心算法能发挥作用的完美土壤。我们不需要动态规划那种复杂的状态转移只需要一个清晰的排序标准和一种“贪心”的选择策略。接下来我就结合具体的题目变体和我的踩坑经验带你彻底掌握它。2. 问题核心与贪心策略的深度解析2.1 问题常见变体与抽象建模在实际比赛中“巧克力”问题可能有几种常见的表述但万变不离其宗。我们先把问题抽象成一个清晰的模型。变体一无限背包贪心可分割这是最经典的一种。假设有n种巧克力第i种巧克力的单价为cost[i]对应的快乐值为happy[i]。你手上有总金额M。目标是最大化总快乐值。关键点在于允许购买非整数份的巧克力。这意味着你可以花一部分钱买某种巧克力的一部分。例如一种巧克力10元一块快乐值30。如果你有5元可以买半块获得15点快乐值。变体二有限数量贪心不可分割这种变体下每种巧克力有库存数量amount[i]。你仍然有总金额M但必须整块整块地购买。目标同样是最大化总快乐值。这更接近一个标准的分数背包问题的整数版本但通过巧妙的贪心策略依然可以解决。变体三满足人数的最小花费另一种常见表述是需要准备一定数量的巧克力比如k块分给小朋友商店有n种包装每种包装包含pieces[i]块巧克力总价为price[i]。你可以购买任意多种包装。目标是恰好或至少满足k块巧克力的需求且总花费最小。这其实是从“最大化快乐值”变成了“最小化成本”但贪心的核心思想——按“性价比”排序——是相通的。无论哪种变体我们首先要做的就是定义“贪心准则”。对于以最大化快乐值为目标的变体最直观的准则就是“单位金额的快乐值”即happy[i] / cost[i]我们称之为“性价比”或“价值密度”。贪心策略告诉我们只要钱还没花完我们就应该优先购买当前性价比最高的巧克力。2.2 为什么贪心策略有效—— 贪心选择性质的证明很多同学会死记“按性价比排序”但一到考场稍微变形的题目就不敢用贪心了。理解“为什么有效”比记住步骤更重要。这里我们用交换论证法来简单证明一下“无限背包、可分割”变体的贪心最优性。假设我们有一个最优解X它没有完全按照性价比从高到低的顺序购买。那么在这个解里必然存在某一时刻我们花了钱在性价比为p_j的巧克力j上而当时还有性价比更高 (p_i p_j) 的巧克力i可供选择可能钱没花完或者买了别的。现在我们尝试进行一个“交换”从购买巧克力j的金额中拿出一小部分δ元转而去购买巧克力i。因为是可分割的这个操作是可行的。交换前这部分δ元产生的快乐值是δ * p_j。 交换后这部分δ元产生的快乐值是δ * p_i。 由于p_i p_j所以δ * p_i δ * p_j。这意味着通过这个交换我们在总花费不变的情况下获得了更多的快乐值。这与X是最优解的假设矛盾。因此任何最优解都必须遵循“优先购买性价比最高巧克力”的策略。换句话说贪心选择是安全的每一步的局部最优买当前性价比最高的能最终导向全局最优。对于“不可分割”的变体这个证明需要稍微调整因为不能随意分割δ元但核心思想一致如果最优解中包含了非性价比最高的物品我们总可以找到一个方法用性价比更高的物品去替换它的一部分或全部从而得到一个不更差的解。严格的证明会用到“拟阵”等概念但在算法竞赛中我们通常可以凭借直觉和对题目数据的观察如成本、价值都是正整数来应用贪心。我的心得遇到新题判断能否用贪心先问自己两个问题1) 问题是否有“最优子结构”大问题的最优解包含小问题的最优解。2) 我的“贪心选择”是否明显且不会被后面的选择推翻“巧克力”问题完美符合。单位价格的快乐值这个标准一旦确定后续无论怎么买都不会后悔之前把钱花在了性价比更高的东西上。3. 算法实现与代码细节拆解理解了策略我们来动手实现。我会以最常见的“无限背包、可分割”变体为例给出详细的代码和注释。假设输入格式是第一行两个整数n巧克力种类数和M总金额接下来n行每行两个整数cost[i]和happy[i]。3.1 数据结构设计与排序首先我们需要一种结构来存储每种巧克力的成本、快乐值和计算出的性价比。为了方便排序我们通常使用一个数组或向量来存储结构体。#include iostream #include vector #include algorithm #include iomanip // 用于输出固定精度 using namespace std; // 定义巧克力结构体 struct Chocolate { int cost; // 单价 int happy; // 快乐值 double ratio; // 性价比happy / cost }; // 比较函数用于按性价比降序排序 bool compare(const Chocolate a, const Chocolate b) { return a.ratio b.ratio; // 性价比高的排在前面 }这里有一个关键细节ratio的数据类型。虽然cost和happy是整数但它们的商可能是小数。使用double类型可以保证排序的准确性。如果使用整数除法并乘以一个很大倍数来比较在极端数据下可能出错不如直接用double省心。3.2 贪心过程的核心循环排序之后贪心的过程就非常直观了遍历这个按性价比排好序的列表尽可能多地购买当前最“划算”的巧克力直到钱花完。int main() { int n, M; cin n M; vectorChocolate chocos(n); // 读入数据并计算性价比 for (int i 0; i n; i) { cin chocos[i].cost chocos[i].happy; // 注意这里需要将整数转换为double再进行除法否则是整数除法 chocos[i].ratio static_castdouble(chocos[i].happy) / chocos[i].cost; } // 按照性价比降序排序 sort(chocos.begin(), chocos.end(), compare); double totalHappy 0.0; // 总快乐值用double存储 int remainingMoney M; // 剩余金额 // 贪心选择 for (int i 0; i n; i) { if (remainingMoney 0) break; // 钱已花完提前结束 // 当前性价比最高的巧克力我们能买多少 // 因为可以分割所以理论上可以买任意金额对应的部分。 // 但在这个循环里我们一次只处理“买光当前巧克力所有能用剩余金额购买的部分”这个概念。 // 更准确地说只要还有钱我们就一直买当前这种巧克力直到钱不够买一整块。 // 对于可分割的情况逻辑可以简化为 if (remainingMoney chocos[i].cost) { // 如果钱够买至少一块我们可以选择买任意整数块但对于最大化快乐值 // 在可分割且单价固定的前提下买多少块都不影响“单价快乐值”。 // 实际上因为可以无限买且可分割我们只需要计算用全部剩余钱能获得多少快乐值。 // 但通常题目会隐含“购买是瞬时的”或者我们这样理解 // 我们优先把钱花在性价比最高的商品上所以应该尽可能多买它。 // 然而在“可分割”设定下更简单的实现是 // 直接计算如果全部剩余钱都买这种巧克力能得到多少快乐值。 // 但这样就不是“贪心过程”了。为了体现“一步一步买”的贪心过程我们这样写 int maxAmount remainingMoney / chocos[i].cost; // 最多能买多少整块 // 由于可分割其实买maxAmount块和把剩余钱全部投入在快乐值计算上等价。 // 但为了逻辑清晰我们假设先买整块。 totalHappy maxAmount * chocos[i].happy; remainingMoney - maxAmount * chocos[i].cost; // 买完整数块后如果还有剩余钱因为可分割我们可以用剩余钱买一部分 // 实际上在上一步当 remainingMoney cost[i] 时循环就会进入下一个性价比更低的商品。 // 但这样对于最后一种商品如果钱不够买一块我们就一点都没买这不对。 // 所以正确的可分割贪心算法应该在钱不够买一整块时用剩余的钱按比例购买。 } // 因此更标准的、处理可分割情况的循环应该这样写 } // 重新设计循环标准的可分割贪心解法 totalHappy 0.0; remainingMoney M; for (int i 0; i n; i) { if (remainingMoney 0) break; if (remainingMoney chocos[i].cost) { // 钱足够全部买成当前巧克力不对我们可能不需要全部钱都买这一种。 // 实际上在可分割问题中我们直到把钱花完为止都应该只买当前性价比最高的。 // 所以当遇到第一种性价比最高的巧克力时我们就应该把所有钱都用来买它。 // 但这显然不对因为如果第一种巧克力库存有限本题无限我们才需要看第二种。 // 所以对于“无限供应、可分割”的模型最优策略就是把所有钱都用来买性价比最高的那种巧克力 // 等等这引出了一个重要点如果性价比最高的巧克力单价是10元快乐值30性价比3。 // 第二种巧克力单价5元快乐值14性价比2.8。 // 我有100元。全买第一种快乐值300。但如果我买第一种90元9块快乐值270剩下的10元买第二种2块快乐值28总快乐值298300。 // 所以确实应该全买性价比最高的。除非...题目有“每种巧克力必须买整数块”的限制但我们已经假设可分割。 // 对于真正的“分数背包问题”物品是可分割的那么最优解就是尽可能多地拿价值密度最高的物品拿完后如果背包还有容量钱还有剩余再拿次高的。 // 但在这个例子里因为第一种物品无限供应且可分割100元可以全部买第一种不需要第二种。所以循环一次就够了。 // 这让我意识到我最初的“循环购买”思路是针对“每种巧克力有数量限制”的变体。对于无限可分割答案太简单。 } }看上面的代码和注释暴露了一个典型的思维误区。我们往往会把不同变体的问题实现搞混。让我们重新梳理并给出两个不同变体的正确代码。变体一无限可分割的代码实现实际上由于可分割且无限供应最优策略就是将所有钱M都用来购买性价比最高 (ratio最大)的那种巧克力。因为任何将钱分给性价比更低的巧克力的行为都会降低总快乐值根据之前的证明。所以代码非常简单// 假设chocos已按ratio降序排序 double maxRatio chocos[0].ratio; double totalHappy M * maxRatio; // 总快乐值 总金额 * 最高性价比 cout fixed setprecision(1) totalHappy endl;但竞赛题为了增加难度通常不会出这么直接的。更常见的是下面这种。变体二有限数量可分割的代码实现这才是上面“循环购买”逻辑正确应用的场景。每种巧克力有最大购买数量amount[i]可能是无限也可能是有限。我们优先买性价比高的直到它被买完或钱花光再考虑下一个。#include iostream #include vector #include algorithm #include iomanip using namespace std; struct Chocolate { int cost; int happy; double ratio; int amount; // 库存数量如果为-1表示无限 }; bool compare(const Chocolate a, const Chocolate b) { return a.ratio b.ratio; } int main() { int n, M; cin n M; vectorChocolate chocos(n); for (int i 0; i n; i) { cin chocos[i].cost chocos[i].happy chocos[i].amount; // 假设输入包含amount chocos[i].ratio static_castdouble(chocos[i].happy) / chocos[i].cost; } sort(chocos.begin(), chocos.end(), compare); double totalHappy 0.0; int remainingMoney M; for (int i 0; i n remainingMoney 0; i) { // 计算当前巧克力最多能买多少考虑库存和资金 int availableByMoney remainingMoney / chocos[i].cost; int canBuy availableByMoney; if (chocos[i].amount ! -1) { // 如果库存有限 canBuy min(canBuy, chocos[i].amount); } // 如果可分割我们实际上计算的是“能买多少价值单位”。 // 但在这个循环里canBuy代表最多能买多少整块。对于可分割最后剩余的钱可以买一部分。 // 更精确的做法是在循环中只处理整块购买最后剩余的钱在循环外处理不对。 // 标准分数背包贪心解法如下 if (canBuy 0) { // 购买整块部分 int buyAmount canBuy; totalHappy buyAmount * chocos[i].happy; remainingMoney - buyAmount * chocos[i].cost; // 如果该商品库存有限记得减少库存本题可能不需要输出购买明细所以可省略 } // 循环结束后如果还有剩余钱理论上应该继续购买下一种商品的可分割部分。 // 但在这个模型里因为我们在每次迭代中都尽可能多地购买当前商品整块 // 剩余的钱已经不足以再购买一块当前商品了所以会进入下一个商品继续尝试购买整块。 // 对于可分割商品这个逻辑是没问题的因为它等价于优先用全部钱买第一种整块部分 // 买不完的部分因为库存没了再用剩余钱买第二种以此类推。 } // 但是上面的写法在处理“可分割”时只买了整块没有处理“最后一点钱买部分商品”的情况。 // 实际上对于有限库存且可分割的商品正确的做法是在循环中判断 // 如果当前商品还有库存但剩余的钱不够买一整块则用剩余的钱按比例购买一部分然后结束。 // 让我们重写这个循环 } // 正确的、通用的“有限库存、可分割”贪心算法实现 totalHappy 0.0; remainingMoney M; for (int i 0; i n remainingMoney 0; i) { if (chocos[i].amount 0) continue; // 库存为0跳过 if (chocos[i].amount -1) { // 无限供应 // 无限供应且可分割那么所有剩余钱都应买这一种因为它是当前性价比最高的 totalHappy remainingMoney * chocos[i].ratio; remainingMoney 0; // 钱花光了 break; } else { // 有限供应 // 计算最多能买多少考虑库存和资金 int maxBuyByMoney remainingMoney / chocos[i].cost; int canBuy min(maxBuyByMoney, chocos[i].amount); if (canBuy 0) { // 购买整块部分 totalHappy canBuy * chocos[i].happy; remainingMoney - canBuy * chocos[i].cost; chocos[i].amount - canBuy; // 更新库存如果题目需要 } // 购买整块后如果还有库存且还有钱但不够买一整块了则购买部分 if (remainingMoney 0 chocos[i].amount 0) { // 剩余的钱全部用来买这种巧克力的部分 double fraction static_castdouble(remainingMoney) / chocos[i].cost; totalHappy fraction * chocos[i].happy; remainingMoney 0; // 钱花光 break; } // 否则钱花光或库存没了继续循环看下一个商品 } } cout fixed setprecision(1) totalHappy endl;这段代码才是处理“有限库存、可分割”变体的正确逻辑。它严格遵循了贪心策略只要当前性价比最高的商品还有库存就尽可能多地购买先整块最后可能买一部分直到钱花光或该商品售罄再考虑下一个性价比低的。我的踩坑心得数据类型是魔鬼totalHappy务必用double。即使题目最终要求输出整数中间计算过程用浮点数也能避免整数除法带来的精度损失。输出时再用setprecision控制。边界条件循环条件remainingMoney 0和i n缺一不可。要小心库存为0的情况。理解“可分割”这是贪心成立的关键。如果题目明确“必须整块购买”那么这就是一个整数背包问题贪心可能得不到最优解但有时数据弱也能过。必须仔细审题。排序是关键一定要确保是按happy/cost的降序排序。如果cost可能为0免费巧克力需要特殊处理通常免费且快乐的巧克力可以视为性价比无穷大优先处理。4. 典型变体实战满足需求的最小花费现在我们来看另一个高频变体满足人数的最小花费。题目描述需要k块巧克力分给小朋友。有n种包装第i种包装含有pieces[i]块巧克力总价为price[i]。你可以购买任意多种包装。求至少获得k块巧克力的最小花费。贪心策略分析这个问题不再是最大化快乐值而是最小化成本。我们还能贪心吗可以但贪心准则变了。我们计算每种包装的“单位巧克力价格”price[i] / pieces[i]。直觉上我们应该优先购买单价最低的包装。但是这里有个陷阱因为包装是整份出售的购买单价最低的包装可能无法恰好凑够k块可能会多买造成浪费。我们需要考虑“浪费”是否会影响最优性。实际上对于“至少k块”的要求我们可以证明在最优解中最多只有一种包装是被“部分利用”的即我们购买的数量不是使总块数刚好大于等于k的那个整数组合中的一部分。更严谨的解法是这是一个完全背包问题的变体k是背包容量块数每种包装的花费是price[i]重量是pieces[i]要求总重量至少为k时的最小花费。这可以用动态规划求解。然而在数据范围较小或者包装种类很少时有一种基于贪心的枚举法非常高效首先将所有包装按单位价格排序。对于单位价格最低的包装我们最多需要ceil(k / pieces[i])份。设这个数量为maxNum。我们枚举购买这种包装的数量x从0到maxNum。对于每一个x我们计算已经花费的钱cost_x x * price[i]和已经获得的巧克力块数got x * pieces[i]。如果got k那么更新答案ans min(ans, cost_x)。如果got k那么剩余需要的块数是remain k - got。对于剩余的需求我们贪心地从单位价格次低的包装开始尽可能少地购买因为目标是总花费最小且我们已经固定了第一种包装的购买量。但注意对于剩余需求我们依然可以继续枚举第二种包装的购买数量或者更简单对于剩余需求我们只考虑“再买一份”能覆盖剩余需求的包装中总花费最小的那个。这是一个子问题。实际上更清晰的思路是由于包装种类n通常很小比如不超过10我们可以用**深度优先搜索(DFS)**枚举每种包装购买的数量配合剪枝如果当前花费已经超过已知最优解则返回。这比纯粹的贪心更稳妥。但是如果题目保证“每种包装的块数pieces[i]是k的约数”或者有其他特殊性质贪心优先买单价最低的可能就是正确的。在竞赛中如果无法证明贪心正确性但数据范围允许n很小用DFS枚举是更保险的做法。代码框架DFS枚举法#include iostream #include vector #include algorithm #include climits using namespace std; int n, k; vectorint pieces, price; long long ans LLONG_MAX; // 最小花费用long long防溢出 // dfs参数当前处理到第idx种包装当前已获得巧克力总块数got当前总花费cost void dfs(int idx, int got, long long cost) { // 剪枝1如果花费已经超过当前最优解直接返回 if (cost ans) return; // 剪枝2如果已经满足需求更新答案并返回 if (got k) { ans min(ans, cost); return; } // 如果所有包装都考虑完了但还没满足需求直接返回这条路径不合法但会被剪枝1或2处理 if (idx n) return; // 计算当前包装最多需要买多少份上限至少要满足剩余需求但也可以多买。 // 上限可以设为剩余需求除以每份块数向上取整但为了枚举完整性可以设一个稍大的数比如10。 // 更合理的上限是使总块数超过k所需的最大份数。 int maxBuy (k - got pieces[idx] - 1) / pieces[idx]; // 向上取整 // 为了剪枝我们也可以从0枚举到maxBuy for (int buy 0; buy maxBuy; buy) { dfs(idx 1, got buy * pieces[idx], cost buy * price[idx]); } } int main() { cin n k; pieces.resize(n); price.resize(n); for (int i 0; i n; i) { cin pieces[i] price[i]; } // 可以按单价排序让DFS优先搜索更优的路径加速剪枝 // 这里简单起见不排序也可以 dfs(0, 0, 0); cout ans endl; return 0; }这个DFS解法虽然是指数复杂度但在n很小≤10且maxBuy不大时是完全可行的。它避免了贪心策略可能出错的风险。5. 常见错误与调试技巧在实现和调试“巧克力”这类贪心算法题时下面这些坑我几乎都踩过。错误1整数除法陷阱这是最最常见的错误。在计算性价比ratio happy / cost时如果happy和cost都是整数在C中/是整数除法会截断小数部分。例如happy3, cost5整数除法结果是0而实际性价比是0.6。这会导致排序完全错误。修正务必使用浮点数除法ratio (double)happy / cost;或ratio happy * 1.0 / cost;。错误2忽略“可分割”与“不可分割”的条件这是策略性错误。如果题目要求必须整块购买不可分割那么这就是一个整数背包问题。贪心算法按性价比排序后尽可能多买得到的不一定是真正的最优解它只是一个近似解或称为“贪心解”。例如总金额 M 6巧克力A成本3快乐值5性价比≈1.667巧克力B成本4快乐值6性价比1.5巧克力C成本2快乐值3性价比1.5 贪心策略会先买一块A花3得快乐5剩余3元。然后性价比最高的是A(已无钱购买整块)和C(1.5)。剩余3元可以买一块C花2得3最后剩1元什么也买不了。总快乐值8。 但最优解是买三块C花6得9。所以贪心错了。修正仔细审题如果明确“必须整块购买”且数据范围不大应使用动态规划0-1背包或完全背包。如果数据范围大贪心可能只是部分分策略。错误3精度问题即便使用了double在比较两个浮点数ratio是否相等或排序时也可能因为精度问题导致不稳定。在极端情况下如果happy和cost都很大double除法的精度损失可能会影响排序顺序。修正一种更稳健的方法是避免直接比较浮点数。我们可以比较交叉相乘的结果要比较a.happy / a.cost和b.happy / b.cost等价于比较a.happy * b.cost和b.happy * a.cost。这样可以在整数域内比较避免精度问题。比较函数可以写成bool compare(const Chocolate a, const Chocolate b) { // 按性价比降序排序使用交叉相乘避免浮点误差 return (long long)a.happy * b.cost (long long)b.happy * a.cost; }注意使用long long防止乘法溢出。错误4处理零成本或零快乐值如果某种巧克力成本为0免费那么其性价比是无穷大或未定义。这需要在排序前特殊处理。通常免费的巧克力应该优先全部拿走因为它们能提供快乐值而不消耗资金。可以在读入数据时将成本为0的巧克力快乐值直接加到总快乐值中并将该种巧克力从列表中移除。 如果快乐值为0那么性价比为0这种巧克力永远不应该购买可以直接忽略。调试技巧构造极端数据自己设计小数据测试特别是边界情况。钱为0。某种巧克力成本为0。所有巧克力成本都大于总金额M。性价比完全相同的情况。需要验证“可分割”和“不可分割”的输出差异。打印中间变量在排序后打印出每种巧克力的成本、快乐值、性价比确认排序顺序是否符合预期。手动模拟用纸笔跟着你的代码逻辑走一遍小样例这是发现逻辑错误最快的方法。对拍如果你能想到一个暴力解法比如DFS枚举所有购买组合用于小数据可以用它来验证你的贪心算法是否正确。生成大量随机小数据分别用贪心和暴力跑对比结果。6. 贪心算法的思维拓展与相关题目“巧克力”问题是一个绝佳的贪心算法入门案例。掌握它之后你可以尝试解决一系列同类型的贪心问题它们都共享“排序贪心选择”的核心框架。相关题目推荐区间问题区间选点数轴上有若干区间问至少选几个点能使每个区间内都至少有一个点。贪心策略按区间右端点排序每次选当前区间右端点并跳过所有包含该点的区间。最大不相交区间数量给定若干区间求最多能选出多少个互不重叠的区间。贪心策略按区间右端点排序优先选择结束早的区间。排队问题排队打水有n个人打水每个人打水所需时间不同如何安排顺序使所有人的平均等待时间最小贪心策略让打水时间短的人先打。背包问题变种分数背包问题就是“巧克力可分割”问题的通用形式物品可分割贪心按价值重量比排序。部分背包问题与分数背包类似。分配问题均分纸牌/糖果传递通过贪心或数学计算最小传递次数。构造问题合并果子/哈夫曼编码每次选择最小的两堆合并使用优先队列实现。如何培养贪心思维大胆假设小心验证对于一道新题先直觉上想一个“看起来最合理”的贪心策略比如优先选单位价值最高的、结束时间最早的、所需时间最短的。尝试证明用反证法或交换论证法思考这个策略为什么可能正确。如果无法严格证明思考是否存在反例。实践检验如果想不到反例或者题目数据范围暗示贪心可行比如n很大10^5这通常排除了DP那就先实现出来。用自己构造的极端数据和随机小数据测试。总结经验将验证过的贪心策略和对应的问题类型归类记忆。例如“按结束时间排序”常用于区间问题“按单位价值排序”用于分数背包。回到“巧克力”问题它之所以经典是因为它清晰地展示了贪心算法的两个核心要素贪心选择性质每一步选当前性价比最高的不会影响后续最优解和最优子结构做出一次选择后剩下的子问题依然是同类的最优化问题。下次再遇到类似“用有限资源获取最大收益”或“用最小成本满足需求”的问题时不妨先想想能不能定义一个“性价比”或“关键指标”来排序排完序后贪心地处理是否总是最优算法学习很多时候就是通过这样一个又一个具体问题的拆解和反思逐渐内化那种“感觉”。希望这篇长文能帮你把“巧克力”这道题以及背后的贪心思想吃得透透的。
返回列表