day35 C++ 贪心算法 柠檬水找零 根据身高重建队列 用最少数量的箭引爆气球

题目1:860 柠檬水找零

题目链接:柠檬水找零

对题目的理解

一杯柠檬水5美元,顾客按照bills数组上的顺序一次购买一杯,给顾客找零,开始手头是没有零钱的,bills数组中至少有一个元素,数组中的元素只有5,10,20这3个数

只有3种情况,分情况讨论

情况1:bill是5,直接收下

情况2:bill是10,消耗一个5,增加一个10

情况3:bill是20,优先消耗一个10和一个5,如果不够,再消耗3个5

情况1和情况2是固定策略,

情况3这里有贪心算法:因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!

局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。

局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!

伪代码

代码

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five=0;//面值为5的钞票数量
        int ten=0;//面值为10的钞票数量
        int twenty=0;//面值为20的钞票数量
        for(int bill:bills){
            if(bill==5){
                five++;
            }
            // if(bill==10){
            //     if(five==0) return false;
            //     five--;
            //     ten++;
            //     }这两种方式都可以
            if(bill==10){
                if(five>0){
                    five--;
                    ten++;
                }
                else return false;
            }
            if(bill==20){
                //使用10+5这个方式还
                if(ten>0 && five>0){
                    ten--;
                    five--;
                    twenty++;
                }
                else if(five>=3){
                    five-=3;
                    twenty++;
                }
                else return false;
            }
        }
        return true;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

总结

本题是分情况讨论,分析具体情况

题目2:406 根据身高重建队列

题目链接:根据身高重建队列

对题目的理解

数组people中的元素,people[i] = [hi,ki],表示第i个人的身高为hi,前面有ki个身高大于等于hi的人

返回队列数组queue,queue中的元素满足ki符合

例      people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]

queue = [[5,0],[7,0][5,2],[6,1],[4,4],[5,2]]  

这个queue代表

0的身高为5,0前面没有比它高的人了

1的身高是7,1前面没有比他高的人了

2的身高是5,5前面有两个比他高的人(这两个人是0和1,身高分别是5和7)

3的身高是6,6前面只有1个比他高的人(这个人是2,身高是7)

........

自己的思路,但不会编程:

本题需要考虑两点:一是计算其他人的身高与本人身高的差,二是看本人身高排在第几

首先对身高进行排列,按照降序进行排列

然后按照个数进行排序

贪心算法

本题有两个维度:h和k,一定要确定一个维度,然后再按照另一个维度重新排列

首先按照k从小到大排,因为k是代表前面有几个人身高比本人高,所以k应该是逐渐增大的,所以将k从小到大排(k相同的话,身高要从小到大排),发现这样排列不符合条件,身高h和k的维度都没有确定

再尝试按照身高h进行排序,因为比的是前面有几个人的身高大于本人,所以身高应该是逐渐减小的,降序排列身高h相同的话,将k按照从小到大排列),这个可以确定一个维度:身高h

Q:接下来只要以k为下标插入队列即可,为什么能够插入呢?

A:因为插入的元素的身高h一定比前面被插入的身高小,所以被插入的元素的身高相对顺序没有变

如图:

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

局部最优可推出全局最优

使用vector数组

代码

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){//对二维数组里的一维数组进行引用
        //a[0].b[0]代表身高h的维度,a[1],b[1]代表k的维度
        if(a[0]==b[0]) return a[1]<b[1];//如果两个人的身高相同,就按照k进行升序排列
        return a[0]>b[0];//两人身高不同,按照降序排列
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        vector<vector<int>> quque;
        //将数组中的身高按照降序排列
        sort(people.begin(),people.end(),cmp);
        //按照k插入
        for(int i=0;i<people.size();i++){
            int position = people[i][1];//取k作为插入的位置
            quque.insert(quque.begin()+position, people[i]);
        }
        return quque;


    }
};
  • 时间复杂度:O(nlog n + n^2)
  • 空间复杂度:O(n)

注意!!!

使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。

vector的大小有两个维度一个是size一个是capicity,size就是平时用来遍历vector时候用的

而capicity是vector底层数组(就是普通数组)的大小,capicity可不一定就是size。

当insert数据的时候,如果已经大于capicity,capicity会成倍扩容,但对外暴漏的size其实仅仅是+1。

Q:vector底层实现是普通数组,怎么扩容的?

A:重新申请一个二倍于原数组大小的数组,然后把数据都拷贝过去,并释放原数组内存。

如上图所示,原vector中的size和capicity相同都是3,初始化为1 2 3,此时要push_back一个元素4,那么底层其实就要申请一个大小为6的普通数组,并且把原元素拷贝过去,释放原数组内存,注意图中底层数组的内存起始地址已经变同时也注意此时capicity和size的变化

虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t × n)级别的,t是底层拷贝的次数

所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。

使用链表

使用C++中的list(底层链表实现)比vector(数组)效率高得多

代码

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){//对二维数组里的一维数组进行引用
        //a[0].b[0]代表身高h的维度,a[1],b[1]代表k的维度
        if(a[0]==b[0]) return a[1]<b[1];//如果两个人的身高相同,就按照k进行升序排列
        return a[0]>b[0];//两人身高不同,按照降序排列
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        list<vector<int>> queue;//list底层实现是链表,插入效率比vector高很多
        //将数组中的身高按照降序排列
        sort(people.begin(),people.end(),cmp);
        //按照k插入
        for(int i=0;i<people.size();i++){
            int position = people[i][1];//取k作为插入的位置
            std::list<vector<int>>::iterator it=queue.begin();
            while(position--) it++;
            queue.insert(it,people[i]);
        }
        return vector<vector<int>>(queue.begin(),queue.end());


    }
};
  • 时间复杂度:O(nlog n + n^2)
  • 空间复杂度:O(n)

题目3:452 用最少数量的箭引爆气球

题目链接:用最少数量的箭引爆气球

对题目的理解

球的水平直径是[xstart,xend],平面上的各个球的这个直径记录在数组points中,箭从x轴的x处垂直射出(一直向前),如果x在球的直径范围内(xstart<=x<=xend),那么箭可以射中该球,箭的数量没有限制,求解射中所有气球需要箭的最少数量。

贪心算法

局部最优:一支箭要尽可能多地射中重叠气球,全局最优,所有气球都被射中,且使用的箭最少。

如下图所示

为了让气球尽可能的重叠,需要对数组进行排序,因为数组有两个坐标,起始坐标start和终止坐标end,所以可以按照这两个顺序进行排序,那么按照起始坐标start进行排序

注意题目要求的一个很关键的点:xstart<=x<=xend 气球就会引爆,那么说明两个气球的区间分别为[2,3][3,4],这两个气球会被同一支箭射爆,所以判断两个气球是否重叠的时候使用如下代码,该段代码是整个代码的精华所在,,注意判断条件是大于,没有等号,这个原因就是这里的注意;

还有就是本题的难点就在于继续判断第三支箭是否和前两支箭重叠,一定注意更新最小右边界,拿第三支箭的左边界和最小边界去比较,这是这道题目的难点

伪代码

完整代码

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0]<b[0];//将数组的第一个元素按照从小到大排序
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        //首先判断数组是否为空,如果为空,说明不需要箭,返回0,但是这一行没必要,因为数组要求长度大于1
        if(points.size()==0) return 0;
        sort(points.begin(), points.end(), cmp);//对数组的第一个位置的元素按照从小到大进行排序
        int result = 1;//初始化为1,因为至少需要一支箭
        for(int i=1;i<points.size();i++){
            if(points[i][0]>points[i-1][1]) result++;//两个气球不重叠
            //判断下一个气球是否和这两个气球重叠
            else{
                points[i][1] = min(points[i-1][1],points[i][1]);//更新最小右边界
            } 
        }
        return result;

    }
};
  • 时间复杂度:O(nlog n),因为有一个快排
  • 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值