力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现

文章介绍了贪心算法在几类编程问题中的应用,包括跳跃游戏的两种解法,加油站问题的暴力解法和贪心解法,以及分发糖果和柠檬水找零问题的解决方案。这些例子展示了如何通过贪心策略寻找局部最优解并逐步达到全局最优解。
摘要由CSDN通过智能技术生成

贪心算法

  1. 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。如何通过局部最优,推出整体最优。
  2. 贪心算法的套路就是常识性推导加上举反例
  3. 贪心算法解题思路:想清楚局部最优是什么,如果推导出全局最优,就够了。

55. 跳跃游戏

在这里插入图片描述

思路:

  • 每次移动一个单位,取最大跳跃步数,获得最大覆盖范围,再判断该范围是否可以覆盖到终点!
  • 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。

步骤:

  • 下标i每次只能在cover范围内移动
  • cover每次只取max(该元素数值补充后的范围, cover本身范围)
  • 如果cover 大于等于终点下标,直接return true就可以了。

代码:

class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.size() == 1) return true;//单元素数组 肯定可以到达终点
        int cover = 0;
        for(int i=0; i<=cover; i++)//i每次只能在cover范围内移动
        {
            cover = max(i+nums[i], cover);//更新最大覆盖范围
            if(cover >= nums.size()-1) return true;//可以覆盖到终点
        }
        return false;
    }
};

45.跳跃游戏II

在这里插入图片描述

方法一 考虑终点

思路:

  • 如果当前步的最大覆盖距离还没覆盖,以最小的步数增加覆盖范围,即再走一步增加覆盖范围,覆盖范围一旦覆盖了终点,就得到最小步数
  • 贪心算法局部最优:当前可移动距离尽可能大,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数。
  • 需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。

注意:

  • 1.更新下一步覆盖最大距离=max(该元素数值补充后的范围, 本身范围)
  • 2.还有个特殊情况,当移动下标达到了当前覆盖的最远距离下标时:
    • 如果当前覆盖最远距离下标不是集合终点,步数就加一,还需要继续走,然后再判断下一步是否可以到达终点,可以就结束。
    • 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了,直接结束

代码:

class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size() == 1) return true;
        int curdistance = 0;// 当前覆盖最远距离下标
        int nextdistance = 0;// 下一步覆盖最远距离下标
        int leap = 0;// 记录走的最大步数
        for(int i=0; i<nums.size(); i++)
        {
            nextdistance = max(nums[i] + i, nextdistance);// 更新下一步覆盖最远距离下标
            // 当移动下标达到了当前覆盖的最远距离下标时
            if(i == curdistance)
            {
                // 如果当前覆盖最远距离下标不是终点 再走一步 增加覆盖范围
                if(curdistance < nums.size()-1)
                {
                    leap++;
                    curdistance = nextdistance;
                    // 下一步的覆盖范围已经可以达到终点,结束循环
                    if(nextdistance >= nums.size()-1) break;
                }
                else break;// 当前覆盖最远距到达集合终点,直接结束
            }
        }
        return leap;
    }
};

方法二 不终点

思路:
如果不考虑终点,那么移动下标只要遇到当前覆盖最远距离的下标,直接步数加一。因此只要让移动下标最远只能移动到nums.size - 2。当移动下标指向nums.size - 2时,

  • 如果移动下标等于当前覆盖最大距离下标, 需要再走一步,即leap++,最后一步一定可以到终点
  • 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。

关键在于控制移动下标i只移动到nums.size() - 2的位置,所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑终点了

代码:

class Solution {
public:
    //不考虑终点
    int jump(vector<int>& nums)
    {
        int curdistance = 0;
        int nextdistance = 0;
        int leap = 0;
        //关键 i最远指向nums.size()-1  考虑终点i最远指向nums.size()  
        for(int i=0; i<nums.size()-1; i++)
        {
            nextdistance = max(i+nums[i], nextdistance);
            if(i==curdistance)//移动下标等于当前覆盖最大距离下标 需要再走一步
            {
                leap++;
                curdistance = nextdistance;//更新当前覆盖的最远距离下标
            }
        }
        return leap;
    }
};

134. 加油站

在这里插入图片描述

暴力解法 双层循环 for+while

思路: for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while
代码: 力扣可以过用例,但是会超时

class Solution {
public:
    //暴力解法
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for(int i=0; i<cost.size(); i++)
        {
            int res = gas[i] - cost[i];//剩余油量
            int index = (i+1) % cost.size();//行驶起点
            // 模拟以i为起点行驶一圈
            while(res > 0 && index != i)
            {
                res += gas[index] - cost[index];
                index = (index + 1) % cost.size();
            }
            // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
            if(res >= 0 && index == i) return i;
        }
        return -1;
    }
};

贪心算法 全局最优

思路: 直接从全局进行贪心选择,情况如下

  • 情况一:如果gas的总和小于cost总和,无论从哪出发,一定跑不了一圈
  • 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
  • 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。

代码:

class Solution {
public:
    //贪心算法 方法1
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
    {
        int gassum = 0;
        int gasmin = INT_MAX;//从起点出发,记录油箱里的油量最小值
        int res = 0;//油箱剩余油量
        //1.从0开始出发 累加油量 记录油箱剩余油量最小值
        for(int i=0; i<gas.size(); i++)
        {
            res = gas[i] - cost[i];
            gassum += res;
            if(gassum < gasmin) gasmin = gassum;
        }
        //2.gas的总和小于cost总和
        if(gassum < 0) return -1;
        //3.油箱剩余油量最小值≥0 从0出发最后回到0
        if(gasmin >= 0) return 0;
        //4.油箱剩余油量最小值<0 从非0节点出发 寻找该节点
        for(int i=gas.size()-1; i>=0; i--)
        {
            res = gas[i] - cost[i];
            gasmin += res;//负数填平
            if(gasmin >= 0) return i;
        }
        return -1;
    }
};

贪心算法 局部最优→全局最优

思路

  • 如果总油量减去总消耗油量≥零,那么一定可以跑完一圈,说明 各个加油站的**剩油量rest[i]**的和一定大于等于零。
  • i从0开始,累加rest[i],rest[i]=gas[i] - cost[i],剩油量和gassum小于0,说明[0, i]区间都不能作为起始位置,选择该区间任一个位置作为起点,没无法到达 i位置,只能从i+1开始,重新累加油量和
  • 局部最优:当前累加rest[i]和gassum一旦小于0,起始位置至少是i+1,才可以跑完一圈。全局最优:找到可以跑一圈的起始位置。

i+1后面就不会出现更大的负数? 如果出现更大的负数,就更新i,起始位置变成新的i+1。
有没有可能 [0,i] 区间 选某一个作为起点,累加到i,gassum不小于零?

gassum<0,区间和1+区间和2<0,又区间和2>0,所以区间和1<0,那么就会重新选择起始位置直到gassum不小于0,也就是图中假设位置。

代码:

class Solution {
public:
    //贪心算法 方法2
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
    {
        int start = 0;//起始位置 跑完一圈的位置
        int gassum = 0;//当前起始位置的剩余油量 累加rest[i]和
        int totalsum = 0;//跑一圈总耗油量
        for(int i=0; i<gas.size(); i++)
        {
            gassum += gas[i] - cost[i];
            totalsum += gas[i] - cost[i];
            //当前累加rest[i]和 gassum<0
            if(gassum < 0)
            {
                start = i+1;//更新起始位置 也就是可以跑完一圈的位置
                gassum = 0;//剩余油量 累加rest[i]和清空 要重新计算
            }
        }
        if(totalsum < 0) return -1;//总耗油量<0 不可能跑一圈
        return start;
    }
};

135. 分发糖果

在这里插入图片描述
思路,采用了两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

1.先确定右边孩子评分高于左边孩子评分情况,从前向后遍历

如果ratings[i] > ratings[i - 1],那么[i]的糖 一定要比[i - 1]的糖多一个,第i个小孩的糖果数量为candyVec[i] = candyVec[i - 1] + 1
局部最优:只要右孩子评分比左孩子大,右孩子就多一个糖果;全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。

2.再确定左边孩子评分高于右边孩子情况,从后向前遍历

3.为什么不能从前向后遍历呢?

  • rating[3]应该是获得糖果数最多的,依次是 rating[4], rating[5], rating[6]与 rating[2]、 rating[1]并列, rating[0]。如果从前向后遍历,得到的结果是[1, 1, 1, 2, 2, 2, 1],并不符合题目要求。
  • 要让rating[3]获得最多糖果,需要与rating[4]、rating[5]、rating[6]都比较,rating[3]与rating[4]的比较要利用rating[5]与rating[4]的比较结果,从前往后遍历是无法得知rating[3]的评分最高。
  • 因此,要从后向前遍历,rating[3]与rating[4]的比较才能利用rating[5]与rating[4]的比较结果。

4.candyVec[i]选择

  • 如果ratings[i] > ratings[i + 1],此时candyvec[i]有两个选择,一个是从前向后遍历的candyVec[i + 1] + 1,一个是从后向前遍历的candyVec[i]
  • 贪心局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

代码:

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candyVec(ratings.size(), 1);//每个孩子都有一个糖果
        //从前往后 
        for(int i=1; i<ratings.size(); i++)//两个孩子比较
        {
            if(ratings[i] > ratings[i-1]) candyVec[i] = candyVec[i-1] + 1;
        }
        //从后往前
        for(int i=ratings.size()-2; i>=0; i--)//两个孩子比较
        {
            if(ratings[i] > ratings[i+1]) candyVec[i] = max(candyVec[i], candyVec[i+1]+1);
        }
        int result = 0;
        for(int c : candyVec) result += c;
        return result;
    }
};

860.柠檬水找零

在这里插入图片描述

  • 有三种情况:
    情况一:账单是5,直接收下
    情况二:账单是10,消耗一个5,增加一个10
    情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5

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

代码:

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five = 0, ten = 0;
        for(int bill : bills)
        {
            //情况一:账单是5,直接收下
            if(bill == 5) five++;
            //情况二:账单是10,消耗一个5,增加一个10
            if(bill == 10)
            {
                if(five <= 0) return false;//没有零钱找了
                five--;
                ten++;
            }
            //情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
            if(bill == 20)
            {
                if(five > 0 && ten > 0)//有5和10的零钱
                {
                    ten--;
                    five--;
                }
                //没有10 但有3个5
                else if(five >= 3) five -= 3;
                else return false;
            }
        }
        return true;
    }
};

406.根据身高重建队列

在这里插入图片描述

思路:

  • 按照身高从大到小排序,身高相同的话则k小的站前面,让高个子在前面。然后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
  • 在按照身高从大到小排序后,贪心算法的局部最优:优先按身高高的people的k来插入,插入操作过后的people满足队列属性;全局最优:最后都做完插入操作,整个队列满足题目队列属性

过程:

  1. 排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]
  2. 插入的过程:
  • 插入[7,0]:[[7,0]]
  • 插入[7,1]:[[7,0],[7,1]]
  • 插入[6,1]:[[7,0],[6,1],[7,1]]
  • 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
  • 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
  • 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

代码:

  • 数组实现
class Solution {
public:
    static bool cmp(const vector<int>& a, const 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) {
        //1.排序
        sort(people.begin(), people.end(), cmp);
        //2.数组
        vector<vector<int>> que;
        for(int i=0; i<people.size(); i++)
        {
            int position = people[i][1];//队列插入位置
            que.insert(que.begin()+position, people[i]);
        }
        return que;
    }
};
  • 链表实现
class Solution {
public:
    static bool cmp(const vector<int>& a, const 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) {
        //1.排序
        sort(people.begin(), people.end(), cmp);
        //3.链表实现
        list<vector<int>> que;
        for(int i=0; i<people.size(); i++)
        {
            int position = people[i][1];
            std::list<vector<int>>::iterator it = que.begin();//起始迭代器
            //找到插入位置 链表只能依次访问
            while(position--)
            {
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());//转换类型
    }
};

使用动态数组vector来insert很费时,如果插入元素大于预先普通数组大小,vector底部先扩容,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。链表虽然没有数组访问便捷,但是插入时快很多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值