目录
[1.2跳跃游戏 II](#1.2跳跃游戏 II)
重要的事情说三遍:
(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)
(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)
(●'◡'●)喜欢小邓儿,一键三连哦(❤ ω ❤)

0.引言
经过两周的练习,相信大家对贪心有了进一步理解🤭,现在,让咱们再来一轮贪心算法练习,来巩固,废话不多说,咱们上题解👇👇👇
1.例题详解
1.1分糖果问题
题目:
一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:
每个孩子不管得分多少,起码分到一个糖果。
任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)
给定一个数组 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
给定一个长度为
n
的 0 索引 整数数组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单调递增的数字
题目:
当且仅当每个相邻位数上的数字
x
和y
满足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]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组
gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-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
。给定两个整数
startValue
和target
。返回显示数字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]
表示水平直径在xstart
和xend
之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标
x
处射出一支箭,若有一个气球的直径的开始和结束坐标为x``start
,x``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
如果想了解更多算法题与思路,欢迎点赞收藏,咱们下周见🤭🤭🤭
