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

文章目录

  • 贪心算法
  • 55. 跳跃游戏
  • 45.跳跃游戏II
    • 方法一 考虑终点
    • 方法二 不终点
  • 134. 加油站
    • 暴力解法 双层循环 for+while
    • 贪心算法 全局最优
    • 贪心算法 局部最优→全局最优
  • 135. 分发糖果
  • 860.柠檬水找零
  • 406.根据身高重建队列

贪心算法

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

55. 跳跃游戏

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

思路:

  • 每次移动一个单位,取最大跳跃步数,获得最大覆盖范围,再判断该范围是否可以覆盖到终点!
  • 贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
    力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第2张图片

步骤:

  • 下标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

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

方法一 考虑终点

思路:

  • 如果当前步的最大覆盖距离还没覆盖,以最小的步数增加覆盖范围,即再走一步增加覆盖范围,覆盖范围一旦覆盖了终点,就得到最小步数
  • 贪心算法局部最优:当前可移动距离尽可能大,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数。
  • 需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第4张图片

注意:

  • 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++,最后一步一定可以到终点
  • 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。
    力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第5张图片

关键在于控制移动下标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. 加油站

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

暴力解法 双层循环 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,才可以跑完一圈。全局最优:找到可以跑一圈的起始位置。
    力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第7张图片

i+1后面就不会出现更大的负数? 如果出现更大的负数,就更新i,起始位置变成新的i+1。
有没有可能 [0,i] 区间 选某一个作为起点,累加到i,gassum不小于零?
力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第8张图片
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. 分发糖果

力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第9张图片
思路,采用了两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

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

如果ratings[i] > ratings[i - 1],那么[i]的糖 一定要比[i - 1]的糖多一个,第i个小孩的糖果数量为candyVec[i] = candyVec[i - 1] + 1
局部最优:只要右孩子评分比左孩子大,右孩子就多一个糖果;全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。
力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第10张图片

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]的比较结果。
    力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第11张图片

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]的糖果多。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
    力扣贪心算法专题(二)55. 跳跃游戏 45.跳跃游戏II 134. 加油站 135. 分发糖果 860.柠檬水找零 406.根据身高重建队列 思路及C++实现_第12张图片

代码:

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.柠檬水找零

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

  • 有三种情况:
    情况一:账单是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.根据身高重建队列

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

思路:

  • 按照身高从大到小排序,身高相同的话则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底部先扩容,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。链表虽然没有数组访问便捷,但是插入时快很多。

你可能感兴趣的:(LeetCode,贪心算法,leetcode,c++)