860. 柠檬水找零 - 力扣(LeetCode)
需要注意的是,我们一开始是没有任何钱的,也就是说我们需要拿着顾客的钱去找零。如果第一位顾客上来就是要找零那么我们无法完成,只能返回false。
分析:
上来我们先不分析贪心算法的内容,先进行一个简单的分类讨论:
1.顾客给一张5块
我们直接收下即可,无需找零
2.顾客给一张10块
我们收下10块并且还需要找零5块
3.顾客给一张20块
我们有两种找零策略:
1)给一张10块加上一张5块
2)给三张5块
所以我们可以看到决定一个顾客是否能成功找零的关键原因就是我们是否有足够多的5块。所以如果我们遇到情况3也就是顾客给了20块的时候,我们应该优先考虑将10块钱先花出去,尽可能的保留多的5块,选择第一种方案。否则就可能遇到找不过来的情况,例如考虑以下两个顾客先后来买,第一个给了20,第二个给了10块。如果我们选择了给三张5块那么就无法找零第二个顾客了。这也是贪心策略在这里的体现,在我们完整的思考过后,其实一般贪心的代码都很好写。
实现:
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0,ten = 0;
for(auto x : bills)
{
if(x == 5) five++;
else if(x == 10)
{
if(five == 0) return false;
five--;ten++;
}
else
{
if(five && ten)
{
ten--;five--;
}
else if(five >= 3)
{
five-=3;
}
else return false;
}
}
return true;
}
};
最后我们来证明以下,这个贪心算法的正确性:
这里我们会用到一个新的证明方法:
交换论证法的步骤:
交换论证法是一种用来证明贪心算法正确性的技巧,主要思路是:
- 假设当前解不是贪心算法的解;
- 如果可以通过交换某些决策将其变成贪心算法的解且不劣化最终结果,则证明贪心解是最优解。
-
贪心策略的设计目标:
- 贪心的核心在于每次找零时优先保存更小面值(尤其是 5 美元)的纸币,因为更小面值的纸币在后续找零中更灵活。
-
假设非贪心策略:
- 如果在某次找零时,没有遵循贪心策略,而是选择了更大的面值(如直接用 3 张 5 美元替代了 10 美元找零)。
- 我们通过交换操作尝试将非贪心解调整为贪心解。
-
交换操作:
- 假设在支付 20 美元时:
- 非贪心策略使用了 3 张 5 美元找零;
- 贪心策略建议使用 1 张 10 美元和 1 张 5 美元找零。
- 如果后续再遇到需要支付 20 美元的顾客,而没有足够的 10 美元,则可能无法完成找零。
- 通过将之前的 3 张 5 美元找零改为 1 张 10 美元和 1 张 5 美元找零,确保后续支付的灵活性不下降。
- 假设在支付 20 美元时:
-
无损性:
- 贪心策略的交换操作不会导致当前的找零失败,同时提高了剩余零钱的灵活性。
-
归纳证明:
- 对于每个顾客的支付:
- 如果选择贪心策略找零,剩余的零钱会是当前最优的(灵活性最大化)。
- 假设前 k−1 个顾客都满足贪心策略的正确性,那么第 k个顾客支付时,贪心策略也能保证正确性。
- 对于每个顾客的支付: