C++算法学习心得七.贪心算法(2)

1.跳跃游戏(55题)

题目描述:

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例  1:

  • 输入: [2,3,1,1,4]
  • 输出: true
  • 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

贪心算法: 跳跃覆盖范围究竟可不可以覆盖到终点,

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)如果 cover 大于等于了终点下标,直接 return true 就可以了。

class Solution {
public:
    bool canJump(vector& nums) {
        int cover = 0;//定义一个覆盖范围
        if(nums.size() == 1)return true;//如果只有一个元素我们直接就返回正确
        //这里需要注意i不能超过cover的覆盖范围,在这里遍历
        for(int i = 0;i <= cover;i++){
            cover = max(i + nums[i],cover);//我们更新下标范围,不断递归覆盖范围里的i和数值之和比大小
            if(cover >= nums.size() - 1)return true;//如果可以覆盖到最后,返回正确
        }
        return false;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

2.跳跃游戏 II(45题)

 题目描述:

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

  • 输入: [2,3,1,1,4]
  • 输出: 2
  • 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳  1  步,然后跳  3  步到达数组的最后一个位置。

说明: 假设你总是可以到达数组的最后一个位置。

贪心算法:

要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖 

class Solution {
public:
    int jump(vector& nums) {
        if(nums.size() == 1)return 0;
        int cur = 0;//当前最大覆盖范围
        int next = 0;//下一个最大最大覆盖范围
        int result = 0;
        //这里注意i的范围
        for(int i = 0;i < nums.size();i++){
            next = max(i + nums[i],next);//覆盖范围的更新
            //如果遍历到最大当前范围
            if(i == cur){
                result++;//我们要记录结果
                cur = next;//把最大范围更新成下一个最大范围
                if(next >= nums.size()-1)break;//如果满足条件跳出
            }
        }
        return result;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

3.K次取反后最大化的数组和(1005题)

题目描述:

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

以这种方式修改数组后,返回数组可能的最大和。

示例 1:

  • 输入:A = [4,2,3], K = 1
  • 输出:5
  • 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。

贪心算法: 

那么本题的解题步骤为:

  • 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K--
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和
class Solution {
//自定义一个函数实现绝对值大小的排序且是倒序
static bool cmp(int a,int b){
    return abs(a) > abs(b);
}
public:
    int largestSumAfterKNegations(vector& nums, int k) {
        sort(nums.begin(),nums.end(),cmp);//按照该规则进行排序
        //这样倒序的排序就可以首先去实现反转数值绝对值最大的数,因为本题是可以对一个数进行多次操作,所以正数就没必要进行多次反转,负数需要全部反转
        for(int i = 0;i < nums.size();i++){
            //循环判断条件,且K次反转
            if(nums[i] < 0 && k > 0){
                nums[i] *= -1;//把数字反转
                k--;//反转次数
            }
        }
        //当全部负数转换为正数,正数情况下对最后一个数进行多次的反转
        if(k % 2 == 1)nums[nums.size()-1] *= -1;
        int result = 0;
        for(int a : nums){
            result += a;//加和操作
        }
        return result;
    }
};
  • 时间复杂度: O(nlogn)
  • 空间复杂度: O(1)

4.加油站(134题)

题目描述:

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

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

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且长度相同。
  • 输入数组中的元素均为非负数。

示例 1: 输入:

  • gas = [1,2,3,4,5]
  • cost = [3,4,5,1,2]

输出: 3 解释:

  • 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
  • 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
  • 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
  • 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
  • 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
  • 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
  • 因此,3 可为起始索引。

暴力写法:for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while,注意下标就可以实现

class Solution {
public:
    int canCompleteCircuit(vector& gas, vector& cost) {
        for(int i = 0;i < cost.size();i++){
            int rest = gas[i] - cost[i];//剩余汽
            int index = (i+1)%cost.size();//这里的下标是用于一个取余的操作来实现,注意i+1是下一个坐标开始记录
            //这里的循环条件汽有剩余并且下标不为i
            while(rest > 0 && index != i){
                rest += gas[index] - cost[index];//下标的剩余汽加入剩余汽
                index = (index+1)%cost.size();//更新下标
            }
            if(rest >= 0 && index == i)return i;//最后判断如果满足条件我们返回下标位置
        }
        return -1;
    }
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)

 回溯算法:

class Solution {
public:
    int canCompleteCircuit(vector& gas, vector& cost) {
        int cursum = 0;
        int min = INT_MAX;
        for(int i = 0;i < gas.size();i++){
            int rest = gas[i] - cost[i];
            cursum += rest;
            if(cursum < min){
                min = cursum;
            }
        }
        if(cursum < 0)return -1;
        if(min >= 0)return 0;
        for(int i = gas.size() - 1;i > 0;i--){
            int rest = gas[i] - cost[i];
            min += rest;
            if(min >= 0){
                return i;
            }
        }
        return -1;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

贪心方法:

局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。 

class Solution {
public:
    int canCompleteCircuit(vector& gas, vector& cost) {
        int cursum = 0;//当前总和
        int totalsum = 0;//总和
        int start = 0;//下标开始位置
        //遍历汽站
        for(int i = 0;i < gas.size();i++){
            cursum += gas[i] - cost[i];//当前汽和消耗之间差的总和
            totalsum += gas[i] - cost[i];//总和
            //如果当前总和小于0,抛弃,从下一个位置开始重新开始
            if(cursum < 0){
                start = i + 1;
                cursum = 0;//将该值归0
            }
        }
        //最后判断条件,如果小于0,返回-1
        if(totalsum < 0)return -1;
        return start;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

5.分发糖果(135题)

题目描述: 

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

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

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:

  • 输入: [1,0,2]
  • 输出: 5
  • 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。

确定右边评分大于左边的情况(也就是从前向后遍历),

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

局部最优可以推出全局最优。

如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1

 确定左孩子大于右孩子的情况一定要从后向前遍历

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。

那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

局部最优可以推出全局最优。

所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多

class Solution {
public:
    int candy(vector& ratings) {
        vectorcandyvec(ratings.size(),1);//这里我们定义一个数组来接收糖果
        //从前向后遍历,注意i的遍历位置
        for(int i = 1;i < ratings.size();i++){
            if(ratings[i] > ratings[i - 1])candyvec[i] = candyvec[i-1]+1;//右边孩子大于左边孩子,糖果+1
        }
        //从后向前遍历,注意i的位置
        for(int i = ratings.size() - 2;i >= 0;i--){
            if(ratings[i] > ratings[i+1])candyvec[i] = max(candyvec[i+1]+1,candyvec[i]);//左边孩子大于右边孩子,且做一个操作取两次遍历的最大值
        }
        int result = 0;//结果
        for(int i = 0;i < candyvec.size();i++){
            result += candyvec[i];//我们结算整个糖果的和
        }
        return result;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

6.柠檬水找零( 860题)

题目描述:

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

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

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

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

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

贪心算法:

只需要维护三种金额的数量,5,10和20。

有如下三种情况:

  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
class Solution {
public:
    bool lemonadeChange(vector& bills) {
        int five = 0,ten = 0,twenty = 0;
        for(int bill : bills){
            if(bill == 5)five++;
            if(bill == 10){
                if(five <= 0)return false;
                ten++,five--;
            }
            if(bill == 20){
                //优先消耗10和5组合
                if(ten > 0 && five > 0){
                    ten--;
                    five--;
                    twenty++;
                }else if(five >=3){
                    five -=3;//消耗三张5
                    twenty++;
                }else{
                    return false;
                }
            }
        }
        return true;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

总结: 

跳跃游戏:此题就是看跳跃范围是否能覆盖终点,每次移动一个单位就更新一个最大覆盖范围即可,i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)如果 cover 大于等于了终点下标,直接 return true 就可以了

跳跃游戏II:要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖 ,需要定义两个覆盖范围,当前覆盖范围和下一个覆盖范围,我们首先需要更新下一个最大覆盖范围,然后按照当前覆盖范围来确定后续,更新当前覆盖范围和结果,满足条件就跳出。

K次取反后最大化的数组和:首先按照绝对值大小来把数组排序,按照从大到小排序,因为要最大和,所以k首先需要反转绝对值大的负数,我们也方便从前向后遍历,我们首先消耗负数,剩余的K去消耗,这个可以多次对一个数进行操作,对k剩下数值判断如果奇数,反转最后一个数,然后得到结果

加油站:暴力的写法就是需要从两个循环来实现,一层循环来遍历整个数组,定义一个初始位置不变,另一个循环去遍历元素,看是否可以回到之前位置,贪心定义两个变量,一个是当前的总和,另一个是总和,如果当前总和小于0,我们从该下标+1处开始下一次遍历,记录下标,如果总和>=0则可以走一圈,返回开始下标就可以

分发糖果:这题我们需要先根据从从左向右遍历,其实需要考虑两个方向的大小都需要满足,所以需要从左向右遍历根据题目条件来规定糖果数量,再根据从右向左遍历根据条件来计算糖果,选择更多的糖果。要注意遍历的开始范围,

柠檬水找零:单纯模拟题,定义三个变量5,10,20,如果只有一张5,直接记录变量,如果是10,我们需要对5--,10++操作,如果20块,则首先考虑一张10和一张5组合,第二种情况是三张5,如果前两种情况都不满足则返回错误,简单题秒了。

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