代码随想录算法训练营第29天 | LeetCode134.加油站、LeetCode135.分发糖果、LeetCode860.柠檬水找零、LeetCode406.根据身高重建队列

目录

LeetCode134.加油站

1. 暴力法

2. 全局最优

3. 贪心算法

LeetCode135.分发糖果

LeetCode860.柠檬水找零

LeetCode406.根据身高重建队列


 

LeetCode134.加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

思路:对于加油站的问题,这里给出了三种解决方法,暴力法,全局最优以及贪心算法。

1. 暴力法

首先对于这道问题其实暴力搜索是可以解决问题的,一个for循环遍历各站点,while循环尝试形成环路,如果最后成功成环,那就返回下标i即可。

这种方法虽然很好想,但是确实时间消耗是比较大的,因此不太推荐使用这种方法。

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int gas_total = 0;//记录总油量
        int index;//记录下标
        for(int i = 0; i < gas.size(); i ++){
            index = i;
            gas_total += gas[i];
            gas_total -= cost[i];
            index ++;
            while(index % gas.size() != i && gas_total > 0){
                //当gas_total=0时,可能会存在多个解,解不唯一就会与题意矛盾
                //题中说解唯一,那么这里的gas_total就不能为0
                //这里是在for循环的下标下尝试做环路绕圈
                gas_total += gas[index % gas.size()];
                gas_total -= cost[index % gas.size()];
                index ++;
            }
            if(index % gas.size() == i && gas_total >= 0) return i;//如果找到了就直接返回下标
            else gas_total = 0;//没有找到就置gas_total为0,准备重新开始下一个加油站测试
        }
        return -1;
    }

时间复杂度:O(n^2)

空间复杂度:O(1)

2. 全局最优

全局最优其实也是一种贪心的思想。

首先对于每一站的剩余油量进行累加,并且使用一个min来记录最小的剩余油量和大小。

当结束循环后,如果说剩余油量和小于0,那就说明总的补充油量是小于总的消耗油量的,不可能存在这样一个站点,使得能够从该站出发,最后绕一个圈回到该出发点,因此直接返回-1;

当最小剩余油量和min大于等于0时,也就是说所记录的每一站的剩余油量和累加后,当然有正有负,但是最后的最小值是大于等于0的,那就是说,从0出发是能够保证环绕一圈回到出发点的,因此最终返回出发点下标为0;

当最小剩余油量和min小于0时,说明从0开始是有问题的,于是这时候开始从最后一个站开始倒着遍历站点,使得min累加每一站剩余油量和,当min大于等于0时,这时候的下标就是出发点的下标,因为从这里出发是能够环绕一圈的。

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curGas = 0;//记录当前的剩余油量
        int min = INT_MAX;//记录当前油量的最小值
        for(int i = 0; i < gas.size(); i ++){
            int res = gas[i] - cost[i];//记录每一站的加油量以及驶向下一站的耗油量的差值
            curGas += res;
            if(curGas < min){
                min = curGas;
            }
        }
        if(curGas < 0) return -1;//所有的油量小于了所有的消耗油量,不管从哪里出发,都没办法绕圈环行
        if(min >= 0) return 0;//因为本身是从0出发的,所以如果记录的最小值剩余油量大于等于0,说明每次都是能够行驶到下一站的
        for(int i = gas.size() - 1; i >= 0; i --){
            int res = gas[i] - cost[i];
            min += res;
            if(min >= 0) return i;
            //当min<0时,从后往前遍历,如果说在某个站的剩余油量能够将min补充,使得其大于等于0,
            //那么就返回该站的序号,该站就是出发时的起始站
        }
        return -1;
    }

时间复杂度:O(n)

空间复杂度:O(1)

3. 贪心算法

上面的全局最优是从整体上看的,其实如果从贪心角度出发,就需要看什么时候是局部最优,以及如何从局部最优推出全局最优。

首先还是从0开始,累加各站剩余油量和,当出现累加和小于0时,为了保证局部最优,所以得从下一个元素开始重新累加各站剩余油量和,前面的油量和也会置为0,重新开始计数,因为在[0,i]这个区间里面已经不存在一个站点,能够使得能成环,所以得尝试下一个i+1位置的元素。

这样,局部最优也就是确保累加和是不小于0的,于是这样的话到最后就能推出全局最优,即能成环,最后返回出发点的索引下标即可。

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curGas = 0;//记录当前剩余油量
        int totalGas = 0;//记录所有油量
        int start = 0;//记录出发点下标
        for(int i = 0; i < gas.size(); i ++){
            curGas += gas[i] - cost[i];
            totalGas += gas[i] - cost[i];
            if(curGas < 0){//当在[0,i]区间出现curGas小于0时,说明此时没有办法到达下一站
                //因此需要重新开始出发,从i+1的地方开始出发,重置curGas=0
                start = i + 1;
                curGas = 0;
            }
        }
        if(totalGas < 0) return -1;//当所有的剩余油量小于0时,说明总加油量少于总消耗油量,没办法从任何一个站出发成功绕圈
        return start;
    }

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode135.分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

思路:分发糖果题目刚拿到的时候一想,其实思路蛮简单的,但是其实里面有很多限制。

变量太多,写的时候总想都控制,但是其实两边都抓不住,会出现各种问题。

因此我们可以先固定一边,考虑完成后,再去完成另一边,最后便能很全面的解决了。

首先我们先考虑右边元素大于左边元素的情况,从前往后遍历,这样就能先更新一下各孩子的糖果数量,而且变量单一后,更加容易控制。

然后我们再考虑左边元素大于右边元素的情况,从后往前遍历,但是这时是需要取更新后的糖果数量和之前从前往后遍历的糖果数量中的最大值,因为只有取最大值,才能满足这两轮遍历的最终效果,否则可能只会满足其中一个,造成情况的出错,答案的错误,多1少1等情况的出现。

这里的遍历顺序其实很有讲究,而且不能反着来,因为在更新糖果数量时,需要用到前一个元素的更新糖果数量的值,如果遍历顺序变了,那最后结果可能会出错。比如考虑右边元素大于左边元素时采用从后往前遍历,那么最后的值是有问题的,可以简单举[1,2,3,4]这个例子手动模拟一下糖果数量,自然就明白这个顺序的意义了。

    int candy(vector<int>& ratings) {
        vector<int> candy(ratings.size(), 1);
        //从前往后遍历,当左孩子评分小于右孩子评分时
        for(int i = 0; i < ratings.size() - 1; i ++){
            if(ratings[i + 1] > ratings[i]) candy[i + 1] = candy[i] + 1;
        }
        //从后向前遍历,当左孩子评分大于右孩子时
        for(int i = ratings.size() - 2; i >= 0; i --){
            if(ratings[i] > ratings[i + 1]) candy[i] = max(candy[i], candy[i + 1] + 1);
            //这里取的是右孩子加1和原先这个孩子所获糖果,两者中的最大值,因为只有最大值才能保证之前的从前往后遍历的结果有效
        }
        int sum = 0;//统计糖果
        for(int i = 0; i < candy.size(); i ++){
            sum += candy[i];
        }
        return sum;
    }

时间复杂度:O(n)

空间复杂度:O(n)

LeetCode860.柠檬水找零

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

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

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

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

思路:柠檬水找零这个问题不难,收的钞票也就是5,10,20三种,当遇到5的账单是就记录下来,遇到10就拿5来找零(如果有的话),遇到20的话情况就有点特殊了,需要优先使用10和5的结合来找零,没有的话再考虑三个5。 因为5不止是20需要找零用,10也需要找零的时候使用5,但是10的话只是在20找零的时候用,所以相比较而言,5更加重要,需要尽可能剩得更多。

    bool lemonadeChange(vector<int>& bills) {
        int cash_5 = 0;//记录5美元零钱的数量
        int cash_10 = 0;//记录10美元零钱的数量
        for(int i = 0; i < bills.size(); i ++){
            if(bills[i] == 5){//当账单为5时
                cash_5 ++;
            }else if(bills[i] == 10){//当账单为10美元时
                if(cash_5 >= 1){
                    cash_5 --;
                    cash_10 ++;
                }else{
                    return false;
                }
            }else if(bills[i] == 20){//当账单为20美元时
                if(cash_5 >= 1 && cash_10 >= 1){
                    //这里优先将10美元找出,然后再是5美元,因为5美元在账单为10美元的地方还会用到
                    cash_5 --;
                    cash_10 --;
                }else if(cash_5 >= 3){
                    cash_5 -= 3;
                }else{
                    return false;
                }
            }
        }
        return true;
    }

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode406.根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

思路:这里和分发糖果得问题很像,变量太多了,想要两手抓,可能无从下手,或者会没办法两头都顾及到,因此只好先固定一边,再考虑另一边

这里先固定的是身高,按照大小从高到低排序(身高相同就按照k小的先排),然后按照所排元素的k值进行位置的插入,这样就能将所有元素按照题意排列完成。

这里为什么先选择身高来排序,而不是k值排序,因为如果按照k值进行排序,最后得出的结果会发现其实身高,以及排好序的k值不是很有序,没办法进行后续操作;相比较而言,按照身高排,后面根据k值选择位置插入,更加具有可操作性,也能在最后得出结果。

    static bool cmp(vector<int>& a, vector<int>& b){
        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>> que;
        sort(people.begin(), people.end(), cmp);//对people排序
        for(int i = 0; i < people.size(); i ++){
            que.insert(que.begin() + people[i][1], people[i]);//按照k的大小对people里面的人插入进行排序
        }
        return que;
    }

 虽然上面的代码比较简洁,但是vector的插入操作其实是比较复杂的,当插入一个元素的时候,在底层是会进行元素的复制移动的,如果说元素大于了原来的尺寸,还会进行扩容,然后复制移动,比较耗时。

所以下面采用list,它的底层实现是链表,相较而言插入操作更加高效。

    static bool cmp(vector<int>& a, vector<int>& b){
        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>> que;//list底层实现是链表,插入效率高于vector 
        sort(people.begin(), people.end(), cmp);//对people排序
        for(int i = 0; i < people.size(); i ++){
            int pos = people[i][1];//获取插入位置
            list<vector<int>>::iterator it = que.begin();
            while(pos --){
                it ++;//寻找插入位置
            }
            que.insert(it, people[i]);//元素插入
        }
        return vector<vector<int>>(que.begin(), que.end());
    }

时间复杂度:O(nlogn+n^2)

空间复杂度:O(n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

 

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值