45m. 跳跃游戏 II
方法一:贪心(自己想的)
用时:10m10s
思路
当前格子curIdx,当前格子的最大跳跃距离nums[curIdx],选择下一步的格子nextIdx,下一步格子的最大跳跃距离nums[nextIdx],在curIdx位置选择nextIdx的贪心逻辑:使nextIdx+nums[nextIdx]最大,这样每一步都能保证是局部最优,即让接下来的两步跳的最远。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
C++代码
class Solution {
public:
int jump(vector<int>& nums) {
if (nums.size() < 2) return 0;
int curIdx = 0;
int res = 0;
while (curIdx + nums[curIdx] < nums.size() - 1) {
int maxIdx = -1;
int nextIdx = -1;
for (int i = curIdx + 1; i <= curIdx + nums[curIdx]; ++i) {
if (i + nums[i] >= maxIdx) {
maxIdx = i + nums[i];
nextIdx = i;
}
}
curIdx = nextIdx;
++res;
}
return res + 1;
}
};
方法二:贪心
用时:1m29s
思路
本质上与方法一一致,都是计算两步的最大距离,但是方法二的实现更简洁。用curDistance
记录上一个格子能到达的最大距离,用maxDistance
记录当前的最大距离,只有当遍历的位置到达curDistance
时,才表明超出上一个格子能跳跃到达的最大范围,此时跳跃步数加一,并更新curDistance
。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
C++代码
class Solution {
public:
int jump(vector<int>& nums) {
int curDistance = 0;
int maxDistance = 0;
int res = 0;
for (int i = 0; i < nums.size() - 1; ++i) {
maxDistance = max(maxDistance, i + nums[i]);
if (i == curDistance) {
curDistance = maxDistance;
++res;
}
}
return res;
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
1005e. K次取反后最大化的数组和
方法一:排序+贪心
用时:14m1s
思路
贪心逻辑:若取反时总是选择最小的数取反。
先将数组排序,然后执行贪心逻辑取反。
- 时间复杂度: O ( n log n ) O(n \log n) O(nlogn)。
- 空间复杂度: O ( log n ) O(\log n) O(logn)。
C++代码
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
int res = 0;
// 排序
sort(nums.begin(), nums.end());
// 将较小的负数取反
for (int i = 0; i < nums.size(); ++i) {
if (k == 0 || nums[i] >= 0) break;
nums[i] *= -1;
--k;
}
// 如果剩余的k为奇数,则把最小的非负数取反
if (k & 1) {
int minNum = 101;
for (int& n : nums) {
res += n;
if (n < minNum) minNum = n;
}
res -= minNum * 2;
} else for (int& n : nums) res += n;
return res;
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
134m. 加油站
方法一:贪心(自己想的)
用时:32m22s
思路
贪心逻辑:若当前油量低于0,则说明从上一个起始位置出发无法环路一周,且不可能从先前的位置出发,所以起始位置只可能是当前位置之后的加油站。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
C++代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
// 若汽油总量小于总开销,则肯定无法环路一周
int totalGas = 0;
for (int& g : gas) totalGas += g;
for (int& c : cost) totalGas -= c;
if (totalGas < 0) return -1;
// 遍历各个加油站
int res = 0;
int curGas = 0; // 当前油量
for (int i = 0; i < gas.size(); ++i) {
curGas += gas[i] - cost[i]; // 更新油量
if (curGas < 0) { // 若当前油量低于0,则说明从上一个起始位置出发无法环路一周
res = i + 1; // 将起始位置改为下一个加油站
curGas = 0; // 重新开始统计
}
}
return res;
}
};
方法二:贪心
思路
方法一中统计总油量和总开销可以放在一个循环中进行。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
C++代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int totalGas = 0;
int res = 0;
int curGas = 0;
for (int i = 0; i < gas.size(); ++i) {
totalGas += gas[i] - cost[i];
curGas += gas[i] - cost[i];
if (curGas < 0) {
res = i + 1;
curGas = 0;
}
}
return totalGas >= 0 ? res : -1;
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
135h. 分发糖果
方法一:
用时:1h13m12s
思路
贪心逻辑:先分别求出满足左侧和右侧要求的糖果数(局部最优),再根据两组糖果数得到满足两侧要求的糖果数(全局最优)。
- 先从左向右遍历,若当前孩子的得分高于左边的孩子,根据题目要求,当前孩子的糖果数等于左边孩子糖果数加一。遍历完后就能得到满足左侧要求的糖果数。
- 同理,从右向左遍历,就能得到满足右侧要求的糖果数。
- 然后根据对应位置的糖果,得到最终的糖果数,对于某个位置的孩子,其糖果数等于满足左侧要求和右侧要求的糖果数中的最大值。
- 最后再统计糖果总数。
代码实现时,2、3、4步可以合并。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
public:
int candy(vector<int>& ratings) {
int size = ratings.size();
vector<int> candyNum(size, 1);
int res = 0;
// 从左往右遍历,记录满足单侧要求的糖果数
for (int i = 1; i < size; ++i) candyNum[i] = ratings[i] > ratings[i - 1] ? candyNum[i - 1] + 1 : 1;
// 从右往左遍历,更新满足两侧要求的糖果数,并计算数量和
for (int i = size - 2; i >= 0; --i) {
candyNum[i] = max(candyNum[i], ratings[i] > ratings[i + 1] ? candyNum[i + 1] + 1 : 1);
res += candyNum[i];
}
return res + candyNum[size - 1];
}
};
方法二:一次遍历
思路
不用拘泥于计算每个位置要分配多少糖果数,只需统计递增序列和递减序列的长度即可。
对于递增序列,新增孩子的糖果数就是递增序列的长度。
对于递减序列,只要递减序列的长度加一,那么总糖果数就会增加序列的长度值,(自己通俗的讲法:因为不知当前孩子是递减序列的倒数第几个,那就先设置为1,如果后续还是递减序列,那么全体递减序列糖果数加一,这样第一个孩子由于是最先入列的,所以肯定是最高的,可以想象成所有人都在长高。)要注意的点是当递增序列的长度和递减序列的长度相等的情况。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
C++代码
class Solution {
public:
int candy(vector<int>& ratings) {
int size = ratings.size();
int increase = 1; // 当前递增序列的长度
int decrease = 0; // 当前递减序列的长度
int pre = 1; // 上一个孩子的糖果数
int res = 1;
for (int i = 1; i < size; ++i) {
if (ratings[i] >= ratings[i - 1]) {
decrease = 0; // 当前不是递减序列,递减序列长度置零
pre = ratings[i] > ratings[i - 1] ? pre + 1 : 1; // 若当前孩子分数比前一个高,则糖果数为前一个孩子的糖果数加1,否则置为1
res += pre; // 总糖果数加上当前孩子的糖果数
increase = pre; // 递增序列的长度与当前孩子的糖果数是相等的
} else { // 递减序列
++decrease;
if (increase == decrease) ++decrease; // 当递增序列的长度和递减序列的长度一致时,此时的递减序列并没有算上极大值点,由于之前递减序列的长度小于递增序列,极大值处的糖果数由递增序列决定;当递减序列长度超过递增序列后,极大值处的糖果数就由递减序列决定了,所以此时递减序列的长度需要加上极大值点
res += decrease; // 递减序列每增加一个长度,则每个位置的糖果数都需要加一,所以糖果数增加量为递减序列的长度
pre = 1; // 递减序列新增的孩子的糖果数恒定为1
}
}
return res;
}
};
看完讲解的思考
方法二真妙啊…太牛了…太牛了…
代码实现遇到的问题
自己磕磕绊绊、缝缝补补地写了一大堆,正确解法真简洁。
最后的碎碎念
逐渐找到做贪心题的感觉,但有些贪心题也是真难想啊,感觉贪心题最考智商了,真的是想得到就有,想不到的话一点办法也没有,完全没有套路可言。