目录

题目描述

860. 柠檬水找零

题意分析

每杯柠檬水卖 5 元,顾客按数组给定的顺序排队,每人手里只可能是 5、10、20 三种面额之一,且每人只买一杯。你开始时身上没有任何零钱,收到的钱只能用来给后面的顾客找零。问能否给每一位顾客都正确找零。

把三种面额翻译成收支:收 5 元不用找零,净增一张 5;收 10 元要找 5 元,净增一张 10、减一张 5;收 20 元要找 15 元,只有「一张 10 加一张 5」和「三张 5」两种组合。

全题的分歧点只在收 20 元这一步——它是唯一存在选择的地方,其余两种面额的处理方式是唯一确定的。所以这道题的「算法含量」全部集中在「20 元该怎么找」,其他都是记账。

约束里账单数量最多 10 万,面额只有三种,$O(n)$ 一遍扫描是自然选择。更重要的是,钞票只按张数区分、没有编号,所以状态只需要两个整数:手里 5 元和 10 元各有多少张。20 元收进来之后永远找不出去(题目里没有比 20 更大的面额需要它),根本不用记。

边界:第一位顾客若付 10 或 20,此时手上一分钱没有,必然失败;bills 全是 5 时必然成功;只要中途有任何一次找不开就立刻返回 false,后面的顾客不必再看。

解法:贪心(维护 5/10 张数)

核心思路

先想想有没有必要搜索。收到 20 元时有两种找法,若对每个 20 都分叉,最坏是 $2^{n}$ 条路径,显然不可行。瓶颈在于我们担心「现在这样找,会不会导致后面找不开」。

关键观察:5 元是唯一的万能币,10 元的用途严格窄于 5 元。10 元只能参与「找 20」这一种场合;而 5 元既能找 10、又能三张凑 20、还能与 10 元搭配凑 20。换句话说,任何用得到 10 元的地方都能用两张 5 元替代,反过来则不成立。

由此得到贪心策略:收到 20 元时,优先用「10 + 5」,只有在没有 10 元可用时才退而用「5 + 5 + 5」。理由是这两种找法都花掉了 15 元的面值,但前者只消耗一张 5,后者消耗三张 5;既然 5 元更通用,就该尽量把它留在手里。

严格一点说,交换论证是这样:假设某个最优(可行)方案在某次收 20 时选了三张 5,而当时手上有 10 元。把这次改成「10 + 5」,手上就多了两张 5、少了一张 10。由于 10 元能做的事 5 元都能做(且两张 5 可替代一张 10),改动后的状态在后续每一步的可行性都不弱于原方案。因此优先用 10 元不会把可行解变成不可行解。

不变量:处理完前 k 位顾客后,fiveten 恰好等于「按上述贪心策略操作时手里 5 元和 10 元的张数」,并且这个组合是所有可行操作序列中「后续可行性最强」的一个。只要在某一步连贪心都找不开,就说明任何策略都找不开,可以直接判定失败。

解题步骤

  • 初始化 five = 0ten = 0:开局身无分文,这是题目明说的前提;不要误以为可以自带零钱。
  • 收到 5 元five++,无需找零。这是唯一能增加 5 元的途径,也是整个系统的「补给」。
  • 收到 10 元:必须找出一张 5。five == 0 时直接返回 false——此时无解,因为 5 元只有一种面额可以凑出 5 元的找零。否则 five--ten++。注意 ten++ 不能忘,它是后面找 20 的关键资源。
  • 收到 20 元:先看 ten > 0 && five > 0,成立就 ten--five--。这个分支放在前面,正是贪心的落点:保 5 元。否则退到 five >= 3five -= 3。两者都不成立则返回 false。
  • 两个分支的先后顺序不能交换:若先判 five >= 3,就变成了「优先消耗三张 5」,会在后续需要找 5 元时提前枯竭。
  • 全部处理完返回 true:中途没有任何一次找不开,说明全程可行。

bills = [5, 5, 10, 10, 20] 走一遍,正确答案是 false。

第 1 位付 5:five = 1ten = 0

第 2 位付 5:five = 2ten = 0

第 3 位付 10:five = 2 > 0,找出一张 5,five = 1ten = 1

第 4 位付 10:five = 1 > 0,再找一张 5,five = 0ten = 2

第 5 位付 20:需要找 15。第一个分支要求 ten > 0 && five > 0,此时 ten = 2five = 0,不成立;第二个分支要求 five >= 3,也不成立。返回 false。手上虽然握着两张 10 元共 20 元,却凑不出 15 这个数——这正是本题要考的「面值组合」而非「总金额」。

再看贪心顺序为什么要紧:bills = [5, 5, 5, 10, 20, 5, 10]。前三位付 5 后 five = 3;第 4 位付 10,five = 2ten = 1;第 5 位付 20,按正确策略用「10 + 5」,得 five = 1ten = 0;第 6 位付 5,five = 2;第 7 位付 10,找一张 5,five = 1,成功返回 true。若第 5 位改用三张 5,则 five = 2 - 3 不成立会走到 false 分支(five 只有 2),当场就错判成无解;即便当时 five 够,多消耗两张 5 也会让第 7 位的找零更紧张。

代码实现

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;

        for (int bill : bills) {
            if (bill == 5) {
                five++;
            } else if (bill == 10) {
                if (five == 0) {
                    return false;
                }
                five--;
                ten++;
            } else {
                if (ten > 0 && five > 0) {
                    ten--;
                    five--;
                } else if (five >= 3) {
                    five -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
}
func lemonadeChange(bills []int) bool {
    five, ten := 0, 0

    for _, bill := range bills {
        if bill == 5 {
            five++
        } else if bill == 10 {
            if five == 0 {
                return false
            }
            five--
            ten++
        } else {
            if ten > 0 && five > 0 {
                ten--
                five--
            } else if five >= 3 {
                five -= 3
            } else {
                return false
            }
        }
    }
    return true
}

复杂度分析

  • 时间复杂度:$O(n)$。每位顾客只被处理一次,分支里全是常数次的加减和比较,没有回溯也没有嵌套循环。
  • 空间复杂度:$O(1)$。只用了 fiveten 两个计数器;20 元钞票收进来后永远用不上,连记都不用记,这是把状态压到两个整数的关键。

关键点总结

  • 贪心的成立要靠「资源通用性」的偏序关系:5 元能替代 10 元的一切用途,反之不行,所以优先花掉通用性差的那一种。这个思路可以迁移到一切「多面额/多规格资源分配」的题。
  • 状态设计要问「哪些信息对未来有影响」。20 元不参与任何找零,果断不记;这一步想清楚,状态就从「所有钞票的多重集」坍缩成两个整数。
  • 找零问题看的是面值组合而不是总金额:手握两张 10 元共 20 元,却凑不出 15 元。这是本题最反直觉、也最容易在面试中被追问的一点。
  • 两个分支的判断顺序即是贪心策略本身,代码里 if (ten > 0 && five > 0) 必须排在 else if (five >= 3) 前面,顺序一换算法就错了。
  • 一旦某步找不开就可以立即返回 false,不必继续——因为贪心状态是「后续可行性最强」的,它都失败说明任何方案都失败。
  • 面试视角:这题代码只有十来行,考的是能不能把交换论证说清楚。被问「为什么优先用 10 元」时,答「因为 5 元更万能」还不够,要能补上「把三张 5 换成 10+5 后状态严格不弱」这句。

易错点总结

  • 收 20 时先判 five >= 3[5,5,5,10,20,5,10] 中第 5 位会消耗掉三张 5(若当时够),后面付 10 的顾客找不开,返回 false,而正确答案是 true。
  • 收 10 时忘记 ten++[5,10,5,20] 里第 4 位本可用「10 + 5」,但 ten 一直是 0,只能走三张 5 的分支,five 只有 1,误判为 false。
  • 收 10 时不判 five == 0[10] 会让 five 变成 -1 并返回 true,正确答案是 false。负数一旦出现,后续所有判断全部失真。
  • 用总金额判断能否找零[5,5,10,10,20] 手上有 20 元总额,按总额判断会返回 true,但两张 10 元凑不出 15,正确答案是 false。
  • 收 20 时的第一个分支只判 ten > 0[10,20] 之类构造中若 ten > 0five == 0five-- 会变成 -1,后续误判,正确应当同时要求 five > 0
  • 把 20 元也计入可找零的面额[5,20,20] 中若第三位试图用收到的 20 元参与找零,逻辑上无从下手(找零只需 15),会写出乱七八糟的分支;20 元收下即沉没。
  • 中途找不开时不立即返回而是继续记账[5,10,10,20] 第 3 位就该判 false,若只置一个标志位继续跑,five 会变成负数,后面的分支行为不可预测。
  • 误以为顾客可以买多杯或钞票可以拆分:题目规定每人一杯、面额固定,若按「凑够金额即可」处理,[5,5,10,10,20] 会得到 true 的错误答案。
  • 假设 bills 有序或可以重排:顾客顺序是固定的,把 [10,5] 排序成 [5,10] 会把必然失败的用例算成成功。
  • 返回值语义写反:题目问「能否给每位顾客找零」,中途失败返回 false、全程通过返回 true;把两者写反会让所有用例整体取反。

相似题目

题目 难度 考察点
455. 分发饼干 简单 双排序后双指针贪心,考的是「把最小的饼干给胃口最小的孩子」
122. 买卖股票的最佳时机 II 中等 把总收益拆成每日差值,累加正数即可,贪心的正确性来自可拆分性
55. 跳跃游戏 中等 维护能到达的最远下标,同样是「一遍扫描 + 单一状态量」的可行性判定
376. 摆动序列 中等 只在方向翻转时计数,考的是识别「峰谷」这一局部最优信号
763. 划分字母区间 中等 先预处理每个字母的最后出现位置,再用扫描线切段
452. 用最少数量的箭引爆气球 中等 按右端点排序后贪心射箭,正确性靠「不选最小右端点必不更优」
406. 根据身高重建队列 中等 需要设计双关键字排序 + 按位插入,贪心顺序的推导比本题复杂得多
621. 任务调度器 中等 由最高频任务撑起桶结构,答案是闭式公式与任务总数取较大值
135. 分发糖果 困难 左右各扫一遍取最大值,说明单向贪心不足以同时满足两侧约束
322. 零钱兑换 中等 同为面额组合问题,但面额任意时贪心不成立,必须退回完全背包 DP