860. 柠檬水找零
目录
题目链接:
题目:
解题思路:
代码:
总结:
题目链接:
860. 柠檬水找零 - 力扣(LeetCode)
题目:
解题思路:
设置两个int型变量记录5元和10元的数量,遇到5元直接a++,遇到10元a--,b++;
遇到20:先使用10元的和5元的结合,再使用3张5元;这个过程中10元和20元的都得判断,若不行直接返回false;最终返回true即可
代码:
class Solution {public boolean lemonadeChange(int[] bills) {int a=0,b=0;for(int i=0;i<bills.length;i++){if(bills[i]==5){a++;}else if(bills[i]==10){if(a>=1){a--;b++;}else return false;}else{if(b>=1&&a>=1){a-=1;b--;}else if(a>=3){a-=3;}else return false;}}return true;}
}
柠檬水找零:一次优雅的贪心算法实践
在算法的世界里,有些问题看似简单,却蕴含着深刻的策略思想。“柠檬水找零”(Lemonade Change)就是这样一个绝佳的例子。它不仅是一个经典的贪心算法应用场景,而且其解法思路可以广泛地应用于类似的 “资源分配” 问题中。
我将带你从问题本身出发,分析找零的核心逻辑,探索可能的解法,最终理解并掌握这个只需一次遍历的优雅贪心方案。
一、问题重述:一杯柠檬水引发的思考
首先,让我们清晰地理解问题:
场景:你正在卖柠檬水,一杯售价 5 美元。
支付方式:顾客只能用 5 美元、10 美元 或 20 美元 的纸币来付款。
你的初始状态:你一开始没有任何零钱。
目标:对于每一位前来购买的顾客,你都必须能准确地找零。你需要判断,能否成功地为所有顾客找零。
约束:
你只能用你手头已有的零钱进行找零。
顾客是按顺序前来的,你必须当场解决当前顾客的找零问题。
这个问题的核心在于,你如何管理你手中的零钱,以应对不同面额的付款。
二、初步思考与暴力解法的困境
面对这个问题,最直接的想法是什么?
暴力解法(不可行):我们可以尝试记录下手中所有零钱的具体情况(比如,有几张 5 元,几张 10 元,几张 20 元)。当收到一张新的钞票时,我们尝试所有可能的找零组合,看是否能凑出需要的金额。如果能,就更新手中的零钱状态,继续处理下一位顾客。
为什么不可行?
状态空间巨大:如果顾客很多,你手中的钞票数量会增加,记录所有钞票的状态是不现实的。
效率低下:对于每一笔交易,你都需要探索所有可能的找零组合,这会导致算法的时间复杂度急剧上升。
暴力解法显然不是我们想要的。我们需要一个更高效、更具策略性的方法。
三、核心洞察:贪心策略的选择
让我们来分析一下找零的逻辑,寻找其中的规律。我们有三种付款情况:
顾客支付 5 美元:
找零需求:$5 - $5 = $0。不需要找零。
操作:直接将这张 5 美元收入囊中。这会增加我们未来找零的灵活性。
顾客支付 10 美元:
找零需求:$10 - $5 = $5。需要找给顾客 一张 5 美元。
操作:检查我们手中是否有 5 美元的零钱。
如果有,就找给顾客,同时我们的 5 美元数量减一,10 美元数量加一。
如果没有,交易失败,直接返回 false。
这是唯一的选择,没有其他组合可以凑出 5 美元。
顾客支付 20 美元:
找零需求:$20 - $5 = $15。需要找给顾客 15 美元。
关键决策点:凑出 15 美元有两种方式:
方案 A:一张 10 美元 + 一张 5 美元 (10 + 5 = 15)
方案 B:三张 5 美元 (5 + 5 + 5 = 15)
现在,问题来了:当两种方案都可行时,我们应该选择哪一种?
这就是贪心算法的用武之地。贪心的本质是在每一步都做出当前看起来最优的选择,而不考虑这一步选择对未来的影响,期望由局部最优解导出全局最优解。
那么,对于找零 15 美元,哪个方案是 “局部最优” 的?
5 美元钞票是 “万能” 的:它可以用来找零 5 美元(给 10 元顾客)和 15 美元(给 20 元顾客)。
10 美元钞票的用途有限:它只能用来找零 15 美元(给 20 元顾客)。
如果我们选择方案 B(用三张 5 美元),会过早地消耗掉我们手中 “万能” 的 5 美元。这可能导致后续来了一个支付 10 美元的顾客时,我们没有 5 美元可以找零,从而导致整个交易链失败。
而如果我们选择方案 A(用一张 10 美元和一张 5 美元),我们消耗了一张用途有限的 10 美元,同时只消耗了一张 5 美元。这样可以保留更多的 5 美元,以应对未来更多的找零需求。
结论(贪心选择):当需要找零 15 美元时,应优先使用 “一张 10 美元 + 一张 5 美元” 的组合。只有在没有 10 美元时,才考虑使用 “三张 5 美元” 的组合。
这个选择策略,是解决整个问题的关键。
四、算法设计:一次遍历的优雅实现
基于以上的贪心策略,我们可以设计出一个非常高效的算法:
步骤 1:状态表示我们不需要记录所有钞票,只需要用两个变量来跟踪我们手中关键的零钱数量:
fiveCount:记录手中 5 美元钞票的数量。
tenCount:记录手中 10 美元钞票的数量。
为什么不记录 20 美元? 因为 20 美元的钞票在这个问题中无法用于找零,所以我们不需要关心它的数量。
步骤 2:遍历与决策遍历 bills 数组,对于每一张收到的钞票 bill:
如果 bill == 5:
无需找零。
fiveCount++。
如果 bill == 10:
需要找零 5 美元。
检查 fiveCount 是否大于 0。
如果是,fiveCount--,tenCount++。
如果否,返回 false。
如果 bill == 20:
需要找零 15 美元。
优先执行贪心策略:
检查是否同时拥有 10 美元和 5 美元 (tenCount > 0 && fiveCount > 0)。
如果是,tenCount--,fiveCount--。
如果不满足上述条件,再检查是否拥有至少三张 5 美元 (fiveCount >= 3)。
如果是,fiveCount -= 3。
如果以上条件都不满足,则无法找零,返回 false。
步骤 3:完成遍历如果成功处理完所有顾客的付款,说明我们为每一位顾客都成功找零了。返回 true。
五、代码解析:一行一行读懂它
现在,让我们来解析你提供的这段代码。它完美地实现了上述的贪心算法。
java
运行
class Solution {
public boolean lemonadeChange(int[] bills) {
// 1. 初始化状态变量
// a 用来记录 5 美元钞票的数量
// b 用来记录 10 美元钞票的数量
int a = 0, b = 0;
// 2. 遍历每一位顾客的付款
for (int i = 0; i < bills.length; i++) {
// 情况一:顾客支付 5 美元
if (bills[i] == 5) {
a++; // 无需找零,直接将 5 美元收入囊中
}
// 情况二:顾客支付 10 美元
else if (bills[i] == 10) {
// 检查是否有 5 美元可以找零
if (a >= 1) {
a--; // 给出一张 5 美元
b++; // 收入一张 10 美元
} else {
// 如果没有 5 美元,无法找零,直接返回 false
return false;
}
}
// 情况三:顾客支付 20 美元
else { // bills[i] == 20
// 贪心策略:优先使用 10 + 5 的组合来找零 15 美元
if (b >= 1 && a >= 1) {
a -= 1; // 给出一张 5 美元
b -= 1; // 给出一张 10 美元
}
// 如果无法使用 10 + 5 的组合,再尝试使用 5 + 5 + 5 的组合
else if (a >= 3) {
a -= 3; // 给出三张 5 美元
}
// 如果两种组合都无法满足,说明无法找零
else {
return false;
}
}
}
// 3. 如果成功处理完所有顾客,说明可以为所有人找零
return true;
}
}
代码中的变量名说明:
你提供的代码中使用了 a 和 b 作为变量名,这在算法竞赛中很常见,追求的是简洁。在实际开发中,为了代码的可读性,更推荐使用 fiveCount 和 tenCount 这样的命名。两者在逻辑上是完全等价的。
代码执行流程模拟:让我们用一个例子来走一遍流程:bills = [5, 5, 5, 10, 20]
初始化: a = 0, b = 0
i = 0, bill = 5:
a++。a 变为 1。状态: {5:1, 10:0}
i = 1, bill = 5:
a++。a 变为 2。状态: {5:2, 10:0}
i = 2, bill = 5:
a++。a 变为 3。状态: {5:3, 10:0}
i = 3, bill = 10:
a >= 1 为 true。
a-- (a 变为 2), b++ (b 变为 1)。状态: {5:2, 10:1}
i = 4, bill = 20:
b >= 1 && a >= 1 为 true (b=1, a=2)。
a-- (a 变为 1), b-- (b 变为 0)。状态: {5:1, 10:0}
遍历结束。
函数返回 true。我们成功为所有顾客找零。
另一个失败的例子:bills = [5, 10, 10]
i=0, bill=5: a 变为 1。
i=1, bill=10: a 变为 0, b 变为 1。
i=2, bill=10: a >= 1 为 false。直接返回 false。
六、复杂度分析与总结
复杂度分析
时间复杂度:O(N)。我们只需要对 bills 数组进行一次遍历,N 是顾客的数量。
空间复杂度:O(1)。我们只使用了 a 和 b 两个额外变量来存储状态,与输入数组的大小无关。
总结
这个 “柠檬水找零” 问题是贪心算法的一个绝佳范例。它的优雅之处在于:
策略的明确性:问题的核心决策点非常清晰 —— 如何处理 20 美元的付款。
贪心选择的正确性:通过分析不同面额钞票的 “通用性”,我们可以明确地判断出 “优先使用 10+5” 的策略是局部最优的。
实现的高效性:基于这个明确的贪心策略,我们可以设计出时间复杂度为 O(N)、空间复杂度为 O(1) 的最优解法。
这个问题告诉我们,当遇到需要在多个可行选项中进行选择的场景时,不妨停下来思考一下:哪个选择能为未来保留更多的灵活性?哪个选择的 “通用性” 更强? 遵循这个原则,往往能找到问题的贪心解法。
希望这篇文章能帮助你彻底理解这个问题,并从中体会到贪心算法的魅力!
总结:
这是一道关于找零问题的算法题解。解题思路是用两个变量分别记录5元和10元的数量,根据顾客支付的金额(5/10/20元)进行相应处理:5元直接收下,10元需找5元,20元优先用10+5组合找零,其次用3张5元。如果找不开就直接返回false。代码实现简洁高效,时间复杂度O(n),空间复杂度O(1)。该解法通过贪心策略正确模拟了找零过程,是典型的贪心算法应用场景。