贪心算法 | 每周8题(三)

目录

0.引言

1.例题详解

1.1分糖果问题

[1.2跳跃游戏 II](#1.2跳跃游戏 II)

1.3单调递增的数字

1.4加油站

1.5坏了的计算器

1.6合并区间

1.7无重叠区间

1.8用最少数量的箭引爆气球

2.小结


重要的事情说三遍:

(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)

(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)

(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)

0.引言

经过两周的练习,相信大家对贪心有了进一步理解🤭,现在,让咱们再来一轮贪心算法练习,来巩固,废话不多说,咱们上题解👇👇👇

1.例题详解

1.1分糖果问题

题目:

一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:

  1. 每个孩子不管得分多少,起码分到一个糖果。

  2. 任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)

给定一个数组 arr 代表得分数组,请返回最少需要多少糖果。

要求: 时间复杂度为 O(n) 空间复杂度为 O(n)

数据范围: 1≤n≤1000001≤n≤100000 ,1≤a[i]≤10001≤a[i​]≤1000

示例 1:

复制代码
输入: 
[1,1,2]
复制返回值: 
4
解释: 最优分配方案为1,1,2

示例 1:

复制代码
输入: 
[1,1,1]
复制返回值: 
1
解释: 最优分配方案为1,1,1

❀❀❀思路:先给每一个人分一颗糖果;再从左向右遍历一次,若下一个人得分比当前多,下一个人糖果为当前人糖果+1;再从右向左遍历,若前一个人得分比当前人多,且糖果比当前少,前一个人糖果为当前人+1。最后,返回所有糖果之和。

代码:

cpp 复制代码
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * pick candy
     * @param arr int整型vector the array
     * @return int整型
     */
    int candy(vector<int>& arr) {
       int n=arr.size();
       //给每个人都分一个
        vector<int>candy(n);
        for(auto &e:candy)e=1;

        //从左往右,当下一个得分比当前多,糖果+1
        for(int i=0;i<n-1;i++)
        {
            if(arr[i+1]>arr[i])candy[i+1]=candy[i]+1;
        }
        //从右往左,当前一个得分比当前多,且糖果少,前一个糖果=当前糖果+1;
        for(int i=n-1;i>0;i--)
        {
            if(arr[i-1]>arr[i]&&candy[i-1]<=candy[i])candy[i-1]=candy[i]+1;
        }
        //得到所有糖果
       int ret=0;
        for(auto e:candy)
        {ret+=e;}
        return ret;
    }
};

1.2跳跃游戏 II

给定一个长度为 n0 索引 整数数组 nums。初始位置在下标 0。

每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1

示例 1:

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

示例 2:

复制代码
输入: nums = [2,3,0,1,4]
输出: 2

❀❀❀思路:每一次选择可跳跃的最远距离。定义一个当前位置的下标left,和目前所能到的最远位置right,及最远位置maxpos。每次更新maxpos取maxpos和i+nums[i]的较大者,赋给right,再让left往下遍历。

代码:

cpp 复制代码
class Solution {
public:
    int jump(vector<int>& nums) {
        int ret=0,left=0,right=0,maxpos=0;
        while(left<=right)
        {
            if(maxpos>=nums.size()-1)return   ret; //确保跳到n-1,有效索引(从0开始)为n-1
									     //nums 是整数数组,无需处理 '\0'	
									   //故不能写成maxpos>nums.size()
            for(int i=left;i<=right;i++)
            {
                maxpos=max(maxpos,i+nums[i]);
            }
            left=right+1;
            right=maxpos;
            ret++;
        }
        return -1; //未跳到结尾
    }
};

1.3单调递增的数字

题目:

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

示例 1:

复制代码
输入: n = 10
输出: 9

示例 2:

复制代码
输入: n = 1234
输出: 1234

示例 3:

复制代码
输入: n = 332
输出: 299

❀❀❀思路:当输入"0~9"或者"递增的数"如"1234",返回本身;其他情况,将第一个(重复数字出现)不满足递增的数变减去数值1,其他位置上的数变成9。需要先将数字依次取出,在比较。可以以将数字先转成字符串,再比较;或者先%10再/10取出数字,在比较。

代码:

cpp 复制代码
//法一:转成字符串
class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string s=to_string(n);
        int i=0;
        int m=s.size();
        while(i+1<m&&s[i]<=s[i+1])i++; //寻找单调递增的部分。x <= y 注意==
        if(i+1==m)return n; //全部单调递增,注意是i+1
           
        //贪心
        while(i-1>=0&&s[i]==s[i-1])i--; //eg:12334546
        s[i]--;                        //将重复数字第一次出现的-1
        for(int j=i+1;j<m;j++)
        {
            s[j]='9';                 //后面的数字变成9
        }
        return stoi(s);
    }
};


//法二:用先%10,在/10依次取出
class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        if(n<10)return n;
        vector<int>s;
        while(n)
        {
            int pre=n%10;
            s.push_back(pre);
            n/=10;
        }
        reverse(s.begin(),s.end());
        for(int i=0;i+1<s.size();i++)
        {
            if(s[i]>=s[i+1])
            {
                s[i]--;
                for(int j=i+1;j<s.size();j++)
                {
                    s[j]=9;
                }
                break;  //若不退出,当出现重复数字,会改变后续重复数字
            }
            if(i+1>=s.size())return n;
        }  

        int ret=0;
        for(auto e:s){ret=ret*10+e;}
        return ret;
    }
};

1.4加油站

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

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

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 可为起始索引。

示例 2:

复制代码
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

提示:

  • n == gas.length == cost.length
  • 1 <= n <= 105
  • 0 <= gas[i], cost[i] <= 104
  • 输入保证答案唯一。

❀❀❀思路:每一次可以走的前提是加的油【gas[i]】减去消费的油【cost[i]】+剩余的要大于等于0。从第一次可走的索引地出发,遍历数组,若能形成环,则返回索引;否则返回-1。

🚩tip: 构成环的表示方法index=(i+step)%n;

代码:

cpp 复制代码
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n=gas.size();
        for(int i=0;i<n;i++)
        {
            int step=0;
             int rest=0;
            for(;step<n;step++)
            {
                int index=(i+step)%n;//构成环
                rest+=gas[index]-cost[index];
                if(rest<0)break;
            }
            if(rest>=0)return i;
            i=i+step;//贪心
        }
        return -1;
    }
};

1.5坏了的计算器

题目:

在显示着数字 startValue 的坏计算器上,我们可以执行以下两种操作:

  • **双倍(Double):**将显示屏上的数字乘 2;
  • 递减(Decrement): 将显示屏上的数字减 1

给定两个整数 startValuetarget 。返回显示数字 target 所需的最小操作数。

示例 1:

复制代码
输入:startValue = 2, target = 3
输出:2
解释:先进行双倍运算,然后再进行递减运算 {2 -> 4 -> 3}.

示例 2:

复制代码
输入:startValue = 5, target = 8
输出:2
解释:先递减,再双倍 {5 -> 4 -> 8}.

示例 3:

复制代码
输入:startValue = 3, target = 10
输出:3
解释:先双倍,然后递减,再双倍 {3 -> 6 -> 5 -> 10}.

❀❀❀思路:反面考虑(正面比较复杂)。由target通过/2或者+1,变成startValue。当target<start,该数只能++,一直到target=start,;当target>start,分奇偶,若为偶数就除2,奇数就+1变成偶数再除2,直到target<start,再进入第一步。

代码:

cpp 复制代码
class Solution {
public:
    int brokenCalc(int startValue, int target) {
       int ret=0;
        while(target>startValue)//倒过来,由target到startValue,target可以*2或+1
        {
            if(target%2==0)target/=2; //贪心
            else target+=1;
            ret++;
        }
        while(target<startValue)
        {
            target++;
            ret++;
        }
        return ret;   
    }
};

1.6合并区间

题目:

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

复制代码
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

复制代码
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

示例 3:

复制代码
输入:intervals = [[4,7],[1,4]]
输出:[[1,7]]
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。

❀❀❀思路:1.先排序【在C++中为了方便写代码,咱们按照左端点升序】;2.若下一个区间左端点小于当前右端点,此时,有重叠部分,合并重叠部分,取最大的右端点;3.否则,更新左端点为下一个区间左端点。重复第二步,直至遍历结束,再加上最后的一个区间。

🚩tip:本题是求区间并集,合并的判断条件是if(a<=right)right=max(right,b)

代码

cpp 复制代码
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        //按照左端顶排序
        sort(intervals.begin(),intervals.end());

        int left=intervals[0][0],right=intervals[0][1];
       vector<vector<int>>ret;
        for(int i=1;i<=intervals.size()-1;i++)
        {
            int a=intervals[i][0];
            int b=intervals[i][1];
            
            //合并重合区间
            if(a<=right)right=max(right,b);
            else 
            {
             ret.push_back({left,right});
             //从断开区间再次循环合并
             left=a;
             right=b;   //因此到最后会剩一个区间
            }
        }
        ret.push_back({left,right});
        return ret;
    }
};

1.7无重叠区间

题目:

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

注意 只在一点上接触的区间是 不重叠的 。例如 [1, 2][2, 3] 是不重叠的。

示例 1:

复制代码
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

复制代码
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

复制代码
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

  • 1 <= intervals.length <= 105
  • intervals[i].length == 2
  • -5 * 104 <= starti < endi <= 5 * 104

❀❀❀思路:1.先排序【按左端点升序】;2.若下一个区间左端点小于当前右端点,此时,保留小区间,进行一次删除操作,取最小的右端点。3.若下一个区间左端点大于或等于当前右端点,无需删除,将下一个区间的右端点更新为right,再进行遍历,直至所有区间遍历结束。

🚩tip:1.本题为区间交集,判断条件

if(a<right)

{

right=min(right,b);

ret++;

}

2.本题交点处不算重叠。

代码:

cpp 复制代码
class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(),intervals.end());
        int right=intervals[0][1];
        int ret=0;
        for(int i=1;i<intervals.size();i++)
        {
            int a=intervals[i][0];
            int b=intervals[i][1];
            if(a<right)
            {  
                right=min(right,b);//每次有重叠时,删去大区间(贪心)
                ret++;
            }
           else
             right=b;
        }
        return ret;
    }
};

1.8用最少数量的箭引爆气球

题目:

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

复制代码
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:

复制代码
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:

复制代码
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。

提示:

  • 1 <= points.length <= 105
  • points[i].length == 2
  • -231 <= xstart < xend <= 231 - 1

❀❀❀思路:本题是1.7的应用,本质上思路差不多,也是区间交集的题。

★tip:本题交点处也算重叠。

代码:

cpp 复制代码
class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) 
    {
        int ret=0;
        sort(points.begin(),points.end());
        int right=points[0][1];
        {
            for(int i=1;i<points.size();i++)
            {
                int a=points[i][0];
                int b=points[i][1];
                if(a<=right)
                {
                    right=min(right,b);
                }
                else
                {
                    ret++;
                    right=b;
                }
            }
        }
        return ret+1;
    }
};

2.小结

本周,小邓儿总结了以下贪心算法的常见题型及解题思路,主要内容包括:1.分糖果问题:通过两次遍历满足相邻孩子得分与糖果数的关系;2.跳跃游戏II:每次选择可跳跃的最远距离;3.单调递增数字:将不满足递增的数字减1,后续位变9; 4.加油站问题:判断能否绕行一周的起始点;5.计算器问题:逆向思维处理目标值;6.区间问题:包括合并区间、无重叠区间和引爆气球三类问题,核心是排序后处理区间交集。

本周的就讲解到这里O(∩_∩)O

如果想了解更多算法题与思路,欢迎点赞收藏,咱们下周见🤭🤭🤭

相关推荐
2401_841495643 小时前
【数据结构】最长的最短路径的求解
java·数据结构·c++·python·算法·最短路径·图搜索
小龙报3 小时前
《算法每日一题(1)--- 连续因子》
c语言·开发语言·c++·windows·git·算法·visual studio
夜晚中的人海3 小时前
【C++】滑动窗口算法习题
开发语言·c++·算法
violet-lz4 小时前
数据结构四大简单排序算法详解:直接插入排序、选择排序、基数排序和冒泡排序
数据结构·算法·排序算法
·白小白4 小时前
力扣(LeetCode) ——118.杨辉三角(C++)
c++·算法·leetcode
CoovallyAIHub4 小时前
超越“识别”:下一代机器视觉如何破解具身智能落地难题?
深度学习·算法·计算机视觉
仰泳的熊猫5 小时前
LeetCode:207. 课程表
数据结构·c++·算法·leetcode
liu****5 小时前
19.map和set的封装
开发语言·数据结构·c++·算法
水冗水孚5 小时前
双指针算法在实际开发中的具体应用之代码Review文章字符串的片段分割
算法·leetcode