目录
一、做题心得
感觉贪心这一块不是很容易想到思路,有时候思路来了写下来就很简单,有时候是真难做出来。今天是贪心章节的Part2,共完成了四道相关题目,感觉还是挺有难度的,尤其是对于没有接触过这些类型的题的人。个人感觉贪心是基础算法里偏难的部分了。
话不多说,直接开始今天的内容。
二、题目与题解
题目一:122.买卖股票的最佳时机II
题目链接
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
给你一个整数数组
prices
,其中prices[i]
表示某支股票第i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7 。示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 最大总利润为 4 。示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
题解:贪心
贪心的题重在于想到如何实现局部最优。
实现局部最优要求我们只收集正利润。
这道题分三种情况:
1.单独交易日:记今天价格p1,明天价格p2,那么明天可卖出利润p2 - p1(为负值的话表示亏损)
2.连续上涨交易日:这时候第一天买最后一天卖收益最大,即pn - p1,等价于(p2 - p1) + (p3 - p2)...(pn - pn-1),也就是可以把跨越多天的买卖等价理解成多个相邻两天的买卖
3.连续下降交易日:此时买卖肯定亏损,即不买卖
我们可以把以上情况都等价为相邻两天的买卖的堆加。第二天价格大于第一天,那么利润为正,第一天买,第二天卖。
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int ans = 0;
for (int i = 0; i < n - 1; i++) {
if (prices[i + 1] > prices[i]) { //注意:我们可以把跨越多天的买卖等价理解成多个相邻两天的买卖
ans += prices[i + 1] - prices[i];
}
}
return ans;
}
};
题目二:55. 跳跃游戏
题目链接
给你一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回
true
;否则,返回false
。示例 1:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。示例 2:
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。提示:
1 <= nums.length <= 104
0 <= nums[i] <= 105
题解:贪心
这道题如果是第一次做的话,感觉会很难想到。
随着当前位置的遍历,我们需要不断更新最远可跳达的位置maxpos(每次更新都通过比较,取之间节点i所能跳跃的最大距离i + nums[i]),并找到遍历提前终止的条件即(i > maxpos)当前位置超出了我们所能到达的最大位置,这时它无论如何都跳不到i当前的位置。
感觉这样形容也不容易理解,这里建议直接看代码(含注释):
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
int maxpos = 0; //maxpos表示从数组的开始位置(索引0)到当前遍历到的位置(索引i)为止,能够跳跃到达的最远位置的索引
for (int i = 0; i < n; i++) {
if (i > maxpos) { //如果当前位置超出了我们能够到达的最远位置,则无法到达该位置,更无法到达数组的最后一个位置
return false;
}
else { //0 <= i <= maxpos时,maxpos表示该区间内所有元素中能跳到的最远位置
maxpos = max(maxpos, i + nums[i]); //不断更新最远可跳达的位置
}
}
return true;
}
};
题目三:45.跳跃游戏II
题目链接
给定一个长度为
n
的 0 索引整数数组nums
。初始位置为nums[0]
。每个元素
nums[i]
表示从索引i
向前跳转的最大长度。换句话说,如果你在nums[i]
处,你可以跳转到任意nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达
nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达nums[n - 1]
。示例 1:
输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是2. 从下标为 0 跳到下标为 1 的位置,跳 1步,然后跳 3步到达数组的最后一个位置。示例 2:
输入: nums = [2,3,0,1,4] 输出: 2提示:
1 <= nums.length <= 104
0 <= nums[i] <= 1000
- 题目保证可以到达
nums[n-1]
题解:贪心
有了上边那大题的基础,这道题还是不简单。
思路如下:
- 初始化变量:
n
:数组nums
的长度。ans
:跳跃次数,初始化为 0。curEnd
:当前能够到达的最远位置(下标),初始化为 0(因为从数组的第一个位置开始)。nextEnd
:在遍历过程中,记录通过当前位置i
跳跃能够到达的最远位置(下标),初始化为 0。
- 遍历数组:
- 使用一个循环遍历数组
nums
的每个位置i
(从 0 到n-1
)。 - 在每次循环中,首先更新
nextEnd
,即将nums[i] + i
(当前位置i
加上可以跳跃的最大长度)与nextEnd
比较,取较大值作为新的nextEnd
。这样,nextEnd
始终保持在当前遍历范围内能够到达的最远位置。
- 使用一个循环遍历数组
- 判断是否需要跳跃:
- 如果当前位置
i
等于curEnd
,说明已经到达了当前能够到达的最远位置,此时需要进行一次跳跃。 - 将
ans
(跳跃次数)加 1,并将curEnd
更新为nextEnd
,表示通过本次跳跃,能够到达的新的最远位置。
- 如果当前位置
- 提前结束循环:
- 如果在遍历过程中发现
nextEnd
已经大于或等于数组的最后一个位置n-1
,说明已经能够到达或超过数组的最后一个位置,此时可以提前结束循环,因为无需再进行更多的跳跃。
- 如果在遍历过程中发现
- 返回结果:
- 循环结束后,返回
ans
,即跳跃次数。
- 循环结束后,返回
代码如下:
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if (n <= 1) { //如果数组长度小于等于1,则不需要跳跃
return 0;
}
int ans = 0; //初始化跳跃次数为0
int curEnd = 0; //当前能够到达的最远位置(下标)
int nextEnd = 0; //下一步能够到达的最远位置(下标)
for (int i = 0; i < n; i++) {
nextEnd = max(nums[i] + i, nextEnd); //更新下一步能够到达的最远位置(下标)
if (i == curEnd) { //如果当前位置i等于当前能够到达的最远位置curEnd,说明需要进行一次跳跃,并更新curEnd为nextEnd
ans++;
curEnd = nextEnd;
if (nextEnd >= n - 1) { //如果当前已经能够到达或超过数组的最后一个位置,则提前结束循环
break;
}
}
}
return ans;
}
};
题目四:1005.K次取反后最大化的数组和
题目链接
1005. K 次取反后最大化的数组和 - 力扣(LeetCode)
给你一个整数数组
nums
和一个整数k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。重复这个过程恰好
k
次。可以多次选择同一个下标i
。以这种方式修改数组后,返回数组 可能的最大和 。
示例 1:
输入:nums = [4,2,3], k = 1 输出:5 解释:选择下标 1 ,nums 变为 [4,-2,3] 。示例 2:
输入:nums = [3,-1,0,2], k = 3 输出:6 解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。示例 3:
输入:nums = [2,-3,-1,5,-4], k = 2 输出:13 解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。提示:
1 <= nums.length <= 104
-100 <= nums[i] <= 100
1 <= k <= 104
题解:排序+贪心
这道题就好理解多了,我们需要进行k次正负反转,为了尽可能得到最大的结果,我们肯定尽可能优先将最小的负数转化为正数,这就需要先进行排序。
但是我们也得考虑反转次数k比较多的情况,将负数反转完后,如果剩余的k为偶数,那么没有影响,任意对一个正数进行偶次反转还是原来的数;如果剩余的k为奇数,就需要再将数组排序,将最小的值反转为负数即可。
代码如下:
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
int n = nums.size();
int ans = 0;
sort(nums.begin(),nums.end()); //先排序
for (int i = 0; i < n && k > 0; i++) {
if (nums[i] < 0) { //遍历的当前元素为负数且k大于0时,变号-->负数变为正数
nums[i] = -nums[i];
k--;
}
else { //其余情况:直接退出循环
break;
}
}
sort(nums.begin(),nums.end()); //注意:再次排序(因为负数变为正数后大小顺序会发生改变)
nums[0] = k % 2 == 1 ? -nums[0] : nums[0]; //如果剩余的k是奇数,则将数组中绝对值最小的元素取反。(此时若是偶数,则没影响)
for (int i = 0; i < n; i++) {
ans = ans + nums[i];
}
return ans;
}
};
三、小结
今天的打卡到此结束了,贪心的确很难想出来,后边也会继续加油。最后,我是算法小白,但也希望终有所获。