【贪心算法C++】柠檬水找零,将数组减半的最少操作次数,最优除法,分发饼干,可被三整除的最大和


前言

哈喽各位,这章是有关贪心算法的一些题。小编给大家分享一下,我会从题目,题目示例,算法原理讲解,证明论证以及代码实现这几个方面来展示给大家。(该解法全部使用C++哦,题目源于力扣:[力扣题库]
在这里插入图片描述


一、柠檬水找零

题目:
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例:

示例1:
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例2:
输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。

代码实现:

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five = 0, ten = 0;//计数5和10
        for (auto x : bills)
         {
            if (x == 5)five++;//直接收下
            else if (x == 10)
            {
                if (five == 0)return false;//美元5美元可以找零
                five--;ten++;
            } 
            else {          //20美元的时候
                if (five && ten)//优先找10+5
                {
                    five--;ten--;
                } else if (five >= 3)//最后在找5+5+5
                {
                    five -= 3;
                }
                else return false;
            }
        }
        return true;
    }
};

结果展示:

在这里插入图片描述
算法讲解:

具体策略当顾客给的是20美元时,优先考虑先找10美元在找5美元

顾客给的钱的有三种,但是店员找零的时候却有四种。
在这里插入图片描述
前面两种很简单,如果顾客给的是5美元,那店员直接收下就可以了,不用找零,如果顾客给的是10美元,那么店员就要先判断手里是否有5美元,如果有那就找5美元,没有那就返回false;那如果是20美元呢?这个时候就要分两种情况:

第一种:先找10再找5美元,
第二种:直接找3个5美元。

那那种是最优解呢。这里就体现了贪心。我们先观察店员找零的时候是不是每次都有5美元的参与,那么这个时候是不是就可以判断5美元是不是相比10美元要重要一些,因为当5美元足够多的时候是不是所有找零都可以用5美元解决啊,但是题目说了刚开始手里面是没有钱的。所以这个时候我们是不是可以优先保留5美元。然后考虑把10美元找出去呢?因为它作用不大嘛,所以这里我们优先选择第一种方法,当10美元已经被找完的时候才选择第二种情况啊,

证明策略:

交换论证法:
在这里插入图片描述
blog.csdnimg.cn/direct/3f40713afeda4fa9af25b2b465d41904.png)

我们以20美元为例,分别在贪心解和最优解选择不同的方法。那么这个时候能把最优解替换成贪心就可以证明贪心解就时最优解。

贪心解给出了10+5的方法,然而最优解给出5+5+5的方法。我们以贪心解为准,如果先10+5,那么最优解中是不是有个10美元没有使用呀?这时最优解中5+5+5和后面没有使用的10美元是不是 都在店员哪里。那么这个时候我们就可以先把两个5美元替换成10美元。 那是不是就可以跟贪心解一样了呀,最终都不会影响最优解的正确性。那如果在后面的找零过程中用过10美元了,那也是可以交换的。把10美元拿上来换成2个5美元是不是依旧不影响最终的结果呀。因为两个都已经使用过了,这里交换只是为了于贪心解保持相同。

二、将数组和减半的最少操作次数

  • 题目:
    给你一个正整数数组 nums 。每一次操作中,你可以从 nums 中选择 任意 一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续执行操作)请你返回将 nums 数组和 至少 减少一半的 最少 操作数。
  • 示例:

示例1:
输入:nums = [5,19,8,1]
输出:3
解释:初始 nums 的和为 5 + 19 + 8 + 1 = 33 。
以下是将数组和减少至少一半的一种方法:
选择数字 19 并减小为 9.5 。
选择数字 9.5 并减小为 4.75 。
选择数字 8 并减小为 4 。
最终数组为 [5, 4.75, 4, 1] ,和为 5 + 4.75 + 4 + 1 = 14.75 。
nums 的和减小了 33 - 14.75 = 18.25 ,减小的部分超过了初始数组和的一半,18.25 >= 33/2 = 16.5 。
我们需要 3 个操作实现题目要求,所以返回 3 。
可以证明,无法通过少于 3 个操作使数组和减少至少一半。

示例2:
输入:nums = [3,8,20]
输出:3
解释:初始 nums 的和为 3 + 8 + 20 = 31 。
以下是将数组和减少至少一半的一种方法:
选择数字 20 并减小为 10 。
选择数字 10 并减小为 5 。
选择数字 3 并减小为 1.5 。
最终数组为 [1.5, 8, 5] ,和为 1.5 + 8 + 5 = 14.5 。
nums 的和减小了 31 - 14.5 = 16.5 ,减小的部分超过了初始数组和的一半, 16.5 >= 31/2 = 15.5 。
我们需要 3 个操作实现题目要求,所以返回 3 。
可以证明,无法通过少于 3 个操作使数组和减少至少一半。

代码实现:

class Solution {
public:
    int halveArray(vector<int>& nums) {
        priority_queue<double> heap;//优先级队列--底层就是堆
       double  sum =0.0;
        for(auto x:nums) 
        {
           heap.push(x);
           sum+=x;
        }
        sum/=2.0;
        int count =0;
        while(sum>0)//这里的sum已经是整个数组的一半了
        {
            double t=heap.top()/2.0;
            heap.pop();
            sum-=t;
            count++;
            heap.push(t);//这里不要忘记要把t要放进堆里去哦
        }
        return count ;
    }
};

结果展示:

在这里插入图片描述
讲解算法原理:

解法:贪心算法+大根堆
具体策略:
每次挑选数组中最大的一个数减半直到数组的和至少减少一半为止,但是该怎样做到每次都能挑选到该数组中最大的数呢?如果遍历一遍的话,那时间复杂度是很大的,所以这里用到数据结构中的一个大根堆。

证明方法: 交换论证法
在这里插入图片描述

这里我们只要把我们的最优解转换成贪心解,在不失去它最优性的前提下就可以了那么从上面图中如果前两个数选择的都是一样的,第三个数两种解法选择了不同的数。由于贪心解里面是选择当前数组中最大的数,那么可以推出:x>y。这个时候就要分两种情况了:

第一种:当最优解中x没有被用到,那么这个时候可以直接把y替换成x。原因是在最优解中比x小的数减半都可以让整个数组的和减半,那么这里选择一个比y大的数减半是不是更能让数组减半了呀。

第二种:当最优解中已经使用过x了。那么这个时候也是可以让y替换成y ,那是因为在最优解中,先用任何一个数减半都是不会影响整个操作次数的。那么有同学就要问了,如果在原来的y的后面还有2/y或者4/y呢?那y和x交换之后,那么是不是逻辑就出错了呢?其实我们可以把他们重新排列一下就可以了。

三、最优除法

题目:

给定一正整数数组 nums,nums 中的相邻整数将进行浮点除法。

例如,nums = [2,3,4],我们将求表达式的值 “2/3/4”。
但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。你需要找出怎么添加括号,以便计算后的表达式的值为最大值。

以字符串格式返回具有最大值的对应表达式

注意: 你的表达式不应该包含多余的括号。
示例:

示例1:
输入: [1000,100,10,2]
输出: “1000/(100/10/2)”
解释: 1000/(100/10/2) = 1000/((100/10)/2) = 200
但是,以下加粗的括号 “1000/((100/10)/2)” 是冗余的,
因为他们并不影响操作的优先级,所以你需要返回 “1000/(100/10/2)”。
其他用例:
1000/(100/10)/2 = 50
1000/(100/(10/2)) = 50
1000/100/10/2 = 0.5
1000/100/(10/2) = 2

示例2:
输入: nums = [2,3,4]
输出: “2/(3/4)”
解释: (2/(3/4)) = 8/3 = 2.667
可以看出,在尝试了所有的可能性之后,我们无法得到一个结果大于 2.667 的表达式。

说明:

  • 1 <= nums.length <= 10

  • 2 <= nums[i] <= 1000

  • 对于给定的输入只有一种最优除法。

代码实现:

class Solution {
public:
    string optimalDivision(vector<int>& nums) {
        int n=nums.size();
        if(n==1)
        return to_string(nums[0]);//to_string(nums[0])这里是把nums[0]这个整数转换为字符串表达形式
        if(n==2)
        return to_string(nums[0])+"/"+to_string(nums[1]);
        
        string ret=to_string(nums[0])+"/("+to_string(nums[1]);
        for(int i=2;i<n;i++)//注意这里是从第二个数之后就开始加“/一个数”的
        {
            ret+="/"+to_string (nums[i]); 
        }
        ret+=")";//最后在加上括号
        return ret;
    }
};

结果展示:
在这里插入图片描述
解决策略:
只需要把A后面的数全部扩起来就可以解决了。
证明策略:
在这里插入图片描述
首先是不是所有的除法与可以写成Y/X的形式,要想最终结果最大,无非就两种方法,一是让分子变大,而是让分母变小。我们以上图第一行红色的为例,是不是前两个数可以写成A/B,然后再将后面的数全部加在分子上,然后让最终的结果最大。这就是贪心解,那怎样证明贪心解就是最优解呢?我们在看看蓝色部分表达式。假设贪心解不是最优解,那么这个时候旁边蓝色部分的表达式就是最优解(因为括号加的位置不同所以导致表达式不同)。怎样判断这两个表达谁是最优解呢?这里只需要比较一下就可以验证谁大谁小(注意这里可以通分比较的前提是nums[i] >=2的),大的就是最优解。我们通过通分可以看到贪心解的结果要大,所以贪心解就是最优解。那怎样加括号才能保证贪心解的表达式呢?这里只需要把A后面的数全部括起来就刚好是贪心解的表达式了。感兴趣的小火伴可以下去验证一下。

四、分发饼干

题目:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。

示例:

示例1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。
所以你应该输出 1。

示例2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出 2。


不过示例2呢有四种情况:
1:把1分给1,把2分给2
2:把1分给1,把3分给2
3:把2分给1,把3分给2
4:把3分给1,把 2分给2
那使用那种方法呢?这里采用的第一种。如果这里把3先分给了孩子,那如果后面还有个孩子3的话那是不是这个孩子就分不到饼干呀?

代码实现:

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
       //排序
       sort(g.begin(),g.end());
       sort(s.begin(),s.end());
       int ret=0;

       int n=g.size(),m=s.size();
       for(int i=0,j=0;i<n&&j<m;i++,j++)
       {
        while(j<m&&s[j]<g[i])j++;//如果饼干的数值小于孩子胃口的数值,那就王后面走
        if(j<m)//要对j进行判断,如果不判断那么当j走完了都没有找到匹配的饼干,那这时饼干是不满足孩子胃口值得。但是这里依然ret++了,所以要控制j在有效范围类
        ret++;
       } 
       return ret;

    }
};

运行结果:
在这里插入图片描述

贪心策略:
先排序,针对当前胃口小的孩子挑选饼干,这里又分为两种情况

第一种:饼干能满足孩子的胃口,那直接喂
第二种:饼干不能满足孩子的胃口,那直接删除饼干,因为已经排好序了,如果饼干不满足当前孩子的胃口,那也不会满足后面孩子的胃口,所以直接删掉就行了

在这里插入图片描述
思路:
以上图为例。我们用一个蓝色箭头指向孩子的胃口值,绿色箭头指向饼干的尺寸。如果绿色箭头指向饼干的尺寸能满足蓝色箭头指向孩子的胃口值,那就同时往后移同时计数。当绿色箭头指向3的时候,这时蓝色箭头指向4,但是3<4,不符合题意,所以绿色箭头直接往后走,这是5>4,能满足题意,两个箭头同时往后以同时计数,依次类推。注意:这里一定是先要对两组数值进行排序。如果不先排序,那么每次都要先遍历数组找出饼干的数值
证明策略:
交换论证法:
在这里插入图片描述
这里只需要把最优解转换成贪心解就可以证明贪心解就是最优解。那么对于这里的最优解中的饼干和胃口有着四种关系:

第一种:当饼干被使用了,孩子也得到了饼干,意思就是第一块饼干用来分给后面的孩子,后面的饼干用来分给第一个孩子(这里是为了和贪心解作比较)。像这样(绿色箭头):
在这里插入图片描述
这时是否可以调成贪心解呢?(像蓝色箭头一样)。答案是可以的,因为在排好序的前提下,第一块饼干既然可以分给后面孩子,那么也是可以分给第一个孩子的,因为后面的孩子胃口值比前面的孩子胃口值大,然后再看第三块饼干是否也可以分给第三个孩子呢,这里也是可以的,因为第三块饼干比第一块饼干的尺寸要大,既然第一块都分给第三个孩子,那么第三块饼干也能分给第三个孩子。所以这种情况成立。

第二种情况:
当饼干使用了,但是孩子没有得到饼干。意思就是第一块饼干分给了后面的孩子,第一个孩子没有被分配到饼干。

这里是不是可以把第一个饼干分给第一个孩子(绿色箭头)就满足贪心解了呀?既然都可以分给后面的孩子,那是不是完全可以分给第一个孩子,因为后面孩子的胃口值比第一个孩子的胃口值要大。所以像这种情况也是可以转换为贪心解的。

第三种情况;
当孩子能分配到饼干,但是饼干没有被使用。也是就是从后面的饼干来分配给第一个孩子:但是第一块饼干没有被使用。
在这里插入图片描述
那这时还可以把把最后解调整成贪心解吗?这里也是可以的,这时我们就要又疑问了。第三块饼干是可以分配给第一个孩子,那就我们怎样确定第一块饼干也可以分给第一个孩子呢?万一胃口值要比饼干尺寸大呢?其实这里可以从上面的贪心解得出第一块饼干是可以分给第一个孩子的。既然第一块饼干可以分配第一个孩子,那这里是不是又转换成贪心解了呀?

第四种:
饼干没有被使用,孩子也没有得到饼干。意思就是第一个孩子和第一块饼干都没有被使用
在这里插入图片描述
这里是指的第一块饼干没有被分配,同时第一个孩子也没有被分配到饼干。那这时要想转换成贪心解,那是不是就要让第一块饼干分给第一个孩子呀(绿色虚线箭头)。那这代表着什么呢?意思就是再满足之前的最优解的前提下又多分配给一个孩子。因为在第一块饼干没有被分配的时候就已经是最优解了。这时是不是相当于第一块饼干多分给了一个孩子。所以是不是也是可以满足贪心解的呀!

以上四种情况全部可以证明我们是可以把最优解转换成贪心解的。说明这里的贪心是正确的。

五、可被三整除的最大和

题目:
给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和。

示例:

示例1:
输入:nums = [3,6,5,1,8]
输出:18
解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

示例2:
输入:nums = [4]
输出:0
解释:4 不能被 3 整除,所以无法选出数字,返回 0。

示例3:
输入:nums = [1,2,3,4,4]
输出:12
解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。

提示:

  • 1 <= nums.length <= 4 * 104
  • 1 <= nums[i] <= 104

代码实现:

class Solution {
public:
    int maxSumDivThree(vector<int>& nums) {
        const int INF=0x3f3f3f3f;
        int sum =0,x1=INF,x2=INF,y1=INF,y2=INF;
        for(auto x:nums)
        {
            sum+=x;
            if(x%3==1)//找出最小值和次小值
            {
                if(x<x1) x2=x1,x1=x;
                else if(x<x2) x2=x;
            }
            else if(x%3==2) //找出最小值和次小值
            {
                if(x<y1) y2=y1,y1=x;
                else if(x<y2)  y2=x; 
            }
        }
        if(sum%3==0)return sum;
        else if(sum%3==1)return max(sum-x1,sum-y1-y2);//返回较大的一个数
        else return max(sum-y1,sum-x1-x2);
    }
};

结果展示:
在这里插入图片描述
解决策略:
先把所有的数累加起来除三,然后再根据累加和来判断是否要删除掉一些数
累加和呢这里有三种情况:
在这里插入图片描述

第一种: 累加和可以被3整除,这时不用删除数据,直接返回
第二种: 累加数除3还余1,这时又有两种情况,一种是有一个数%3==1+剩下总数%3=0,另一种则是两个%3= =2的+剩下总数%3=0,这里为什么是两个呢?因为两个余2相加再除3还是余1。最后再返回最大的值就行了。
第三种: 跟第二种相似

这里还有个难点:怎样找到最小的数和次小的数呢?一般情况呢我们会选择排序,当然排序也可以。不过这里我们用另一个分类讨论的方法来解决这个问题而且这个方法的时间复杂度要比一般排序的时间复杂度要小。

第一种:当X<X1<X2时
在这里插入图片描述
第二种: 当X1<X<X2时
在这里插入图片描述

第三种: 当X1<X2<X时
在这里插入图片描述

总结

这是小编最近五天做的贪心题,希望可以帮助到各位!让我们继续加油吧
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值