柠檬水找零问题是一个经典的贪心算法练习题,题目描述如下:假设你经营一家柠檬水摊,每杯柠檬水售价5美元。顾客排队购买,每次只买一杯柠檬水,并支付5美元、10美元或20美元。你必须正确找零(注意:一开始你手头没有任何零钱)。给定一个整数数组bills,其中bills[i]是第i位顾客支付的金额,如果你能给每位顾客正确找零,返回true,否则返回false。
这个问题的核心在于如何高效管理手头的零钱,并在面对不同面额的支付时做出最优的找零决策。作为一道LeetCode中等难度题目,它很好地考察了程序员对贪心算法的理解和实际应用能力。
贪心算法是一种在每一步选择中都采取当前状态下最优的选择,从而希望导致全局最优解的算法策略。对于找零问题,贪心算法特别适用,因为美元的面额设计本身就具有贪心性质 - 即较大的面额是较小面额的整数倍。
在本题中,当收到20美元时,我们优先使用10美元+5美元的组合找零(而不是三张5美元),因为这样可以保留更多的5美元零钱,为后续可能需要的找零做准备。这就是贪心策略的体现。
我们使用一个长度为3的数组ch来记录各面额的数量:
ch[0]:5美元数量ch[1]:10美元数量ch[2]:20美元数量选择数组而不是三个独立变量的好处是代码更简洁,且便于可能的扩展(如果未来面额种类增加)。
cpp复制class Solution {
public:
int ch[3] = {0, 0, 0}; // 分别记录5、10、20的数量
bool lemonadeChange(vector<int>& bills) {
for(int bill : bills) {
if(bill == 5) {
ch[0]++; // 直接收下5美元
}
else if(bill == 10) {
if(ch[0] == 0) return false; // 没有5美元可找
ch[0]--; // 找零一张5美元
ch[1]++; // 收下10美元
}
else { // 处理20美元
// 优先使用10+5的组合找零
if(ch[1] > 0 && ch[0] > 0) {
ch[1]--;
ch[0]--;
}
// 没有10美元,尝试用三张5美元
else if(ch[0] >= 3) {
ch[0] -= 3;
}
// 两种找零方式都不行
else {
return false;
}
ch[2]++; // 收下20美元(虽然不影响找零逻辑)
}
}
return true;
}
};
10美元处理:
20美元处理:
边界条件:
虽然当前实现已经很高效,但还可以做以下微优化:
优化后的代码如下:
cpp复制class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, 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;
}
};
贪心策略错误:
边界条件遗漏:
状态更新错误:
小规模测试用例:
打印中间状态:
在每次处理账单后打印当前各面额的数量,帮助追踪问题:
cpp复制cout << "After " << bill << ": five=" << five << ", ten=" << ten << endl;
虽然题目设定是柠檬水摊,但类似的找零问题在以下场景都很常见:
理解这个算法有助于设计更健壮的支付找零逻辑。
更多面额:
有限零钱:
最优找零:
在实际编码面试中,这类问题考察的不仅是算法实现能力,更重要的是:
我在最初解决这个问题时,曾犯过没有优先使用10+5组合的错误。后来通过以下测试用例发现了问题:
输入:[5,5,10,10,20]
错误解法返回:true(实际应为false)
这个案例教会我在实现贪心算法时,一定要仔细验证"贪心选择"的正确性。有时候看起来合理的局部最优,不一定导致全局最优。