3.19-贪心
121. 买卖股票的最佳时机 - 力扣(LeetCode)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
int min = INT_MAX;
for(int& price : prices){
if(price < min) {
min = price;
continue;
}
res = max(res , price - min);
}
return res;
};
55. 跳跃游戏 - 力扣(LeetCode)
思路:
- 先看示例 2,nums=[3,2,1,0,4]。我们在遍历数组的同时,维护最右可以到达的位置 mx,如下表:
从 0 可以跳到 1,2,3,但是无法从 1,2,3 中的任何位置跳到 4。当我们遍历到 i=4 时,发现
| i | nums[i] | i+nums[i] | mx |
|---|---|---|---|
| 0 | 3 | 3 | 3 |
| 1 | 2 | 3 | 3 |
| 2 | 1 | 3 | 3 |
| 3 | 0 | 3 | 3 |
| 4 | 4 | 8 | 失败 |
i>mx这意味着 i 是无法到达的,返回 false。
- 然后来看示例 1,nums=[2,3,1,1,4],在遍历数组的同时,维护最远可以到达的位置 mx,如下表:
| i | nums[i] | i+nums[i] | mx |
|---|---|---|---|
| 0 | 2 | 2 | 2 |
| 1 | 3 | 4 | 4 |
| 2 | 1 | 3 | 4 |
| 3 | 1 | 4 | 4 |
| 4 | 4 | 8 | 8 |
从 0 可以跳到 1,2,最远可以到达的位置 mx=2。能否跳到更远的位置?那就看从 1 能跳到哪些位置,从 2 能跳到哪些位置。
从 1 可以跳到 2,3,4,mx 更新成 4。
从 2 可以跳到 3,mx 不变。
从 3 可以跳到 4,mx 不变。
到达 4,返回 true。
一般地,算法如下:
- 从左到右遍历 nums,同时维护能跳到的最远位置 mx,初始值为 0。
- 如果 i> mx,说明无法跳到 i,返回 false。
- 否则,用 i+nums[i] 更新 mx 的最大值。如果循环中没有返回 false,那么最后返回 true。
另一种理解方式是,把每个 nums[i]看成闭区间 [i,i+nums[i]],问题变成判定这 n 个区间能否合并成一个大区间,这可以用 56. 合并区间 的算法解决。
class Solution {
public:
bool canJump(vector<int>& nums) {
int mx = 0;
for (int i = 0; i < nums.size(); i++) {
if (i > mx) { // 无法到达 i
return false;
}
mx = max(mx, i + nums[i]); // 从 i 最右可以跳到 i + nums[i]
}
return true;
}
};
45. 跳跃游戏 II - 力扣(LeetCode)
思路:

答疑
问:为什么代码只需要遍历到 n−2?
答:当 i = n−2 时,如果 i < curRight,说明可以到达 n−1;如果 i = curRight,我们会造桥,这样也可以到达 n−1(题目保证一定能到)。所以无论是何种情况,都只需要遍历到 n−2。或者说,n−1 已经是终点了,你总不能在终点还打算造桥吧?
问:我能想出这题的思路,就是代码实现总是写不对,有没有什么建议?
答:清晰的变量名以及一些必要的注释,会对理清代码逻辑有帮助。在出现错误时,可以用一些小数据去运行你的代码,通过 print 或者打断点的方式,查看这些关键变量的值,看看是否与预期结果一致。
问:如果题目没有保证一定能到达 n−1,代码要怎么改?
答:见 1326. 灌溉花园的最少水龙头数目,我的题解。
class Solution {
public:
int jump(vector<int>& nums) {
int ans = 0;
int cur_right = 0; // 已建造的桥的右端点
int next_right = 0; // 下一座桥的右端点的最大值
for (int i = 0; i + 1 < nums.size(); i++) {
// 遍历的过程中,记录下一座桥的最远点
next_right = max(next_right, i + nums[i]);
if (i == cur_right) { // 无路可走,必须建桥
cur_right = next_right; // 建桥后,最远可以到达 next_right
ans++;
}
}
return ans;
}
};
1326. 灌溉花园的最少水龙头数目 - 力扣(LeetCode)
和上一题的区别在于,水龙头的覆盖范围为[i - nums[i] , i + nums[i]],意味着我更新桥的时候可以一步从i - nums[i]处走到i + nums[i]处,也即
next_right = max(next_right, right_most[i]);
所以先预处理一下每个下标i能走到的最右端点:
// 预处理:记录每个左端点能覆盖到的最远右端点
vector<int> right_most(n + 1); // 下标为左端点,值为能覆盖的最远右端点
for (int i = 0; i <= n; i++) {
int left = max(i - r, 0); // 覆盖区间的左端点(不能为负数)
right_most[left] = max(right_most[left], i + ranges[i]); // 更新左端点能覆盖的最远右端点
}
问:为什么没有初始化 rightMost[i]=i?
答:如果可以走到 i,那么 nextRight≥i,此时 rightMost[i]=i 还是 =0 都不会影响 nextRight 的值,也就没必要初始化 rightMost[i]=i 了。
class Solution {
public:
int minTaps(int n, vector<int>& ranges) {
// 预处理:记录每个左端点能覆盖到的最远右端点
vector<int> right_most(n + 1); // 下标为左端点,值为能覆盖的最远右端点
for (int i = 0; i <= n; i++) {
int left = max(i - ranges[i], 0); // 覆盖区间的左端点(不能为负数)
right_most[left] = max(right_most[left], i + ranges[i]); // 更新左端点能覆盖的最远右端点
}
int ans = 0; // 需要的水龙头数量
int cur_right = 0; // 当前已覆盖的最远右端点
int next_right = 0; // 下一步可能覆盖的最远右端点
// 遍历每个位置(只需到 n-1,因为最终要覆盖到 n)
for (int i = 0; i < n; i++) {
next_right = max(next_right, right_most[i]); // 更新下一步能覆盖的最远右端点,这一步和上一题有区别
if (i == cur_right) { // 走到当前覆盖的最右端,必须新增一个水龙头
if (i == next_right) {
// 下一步无法扩展覆盖范围,无法到达终点
return -1;
}
cur_right = next_right; // 更新当前覆盖的最右端
ans++; // 新增一个水龙头
}
}
return ans;
}
};
763. 划分字母区间 - 力扣(LeetCode)
思路:
「同一字母最多出现在一个片段中」意味着,一个片段若要包含字母 a,那么所有的字母 a 都必须在这个片段中。
例如示例 1,s=ababcbacadefegdehijhklij,其中字母 a 的下标在区间 [0,8] 中,那么包含 a 的片段至少要包含区间 [0,8]。
把所有出现在 s 中的字母及其下标区间列出来:
| 字母 | 下标 | 下标区间 |
|---|---|---|
| a | [0,2,6,8] | [0,8] |
| b | [1,3,5] | [1,5] |
| c | [4,7] | [4,7] |
| d | [9,14] | [9,14] |
| e | [10,12,15] | [10,15] |
| f | [11] | [11,11] |
| g | [13] | [13,13] |
| h | [16,19] | [16,19] |
| i | [17,22] | [17,22] |
| j | [18,23] | [18,23] |
| k | [20] | [20,20] |
| l | [21] | [21,21] |
例如字母 d 的区间为 [9,14],片段要包含 d,必须包含区间 [9,14],但区间 [9,14] 中还有其它字母 e,f,g,所以该片段也必须包含这些字母对应的区间 [10,15],[11,11],[13,13],合并后得到区间 [9,15]。
将表格中的区间合并为如下几个大区间:
[0,8],[9,15],[16,23]
这些区间满足「同一字母最多出现在一个片段中」的要求。
由于题目要求划分出尽量多的片段,而我们又无法将上述区间的任何区间划分开,所以合并出的区间长度即为答案。
算法
- 遍历 s,计算字母 c 在 s 中的最后出现的下标 last[c]。
- 初始化当前正在合并的区间左右端点 start=0, end=0。
- 再次遍历 s,由于当前区间必须包含所有 s[i],所以用 last[s[i]] 更新区间右端点 end 的最大值。
- 如果发现 end=i,那么当前区间合并完毕,把区间长度 end−start+1 加入答案。然后更新 start=i+1 作为下一个区间的左端点。
- 遍历完毕,返回答案。
if (end == i) 这一步与上面题的「不得不建桥」相似
class Solution {
public:
vector<int> partitionLabels(string s) {
int n = s.length();
int last[26];
for (int i = 0; i < n; i++) {
last[s[i] - 'a'] = i; // 每个字母最后出现的下标
}
vector<int> ans;
int start = 0, end = 0;
for (int i = 0; i < n; i++) {
end = max(end, last[s[i] - 'a']); // 更新当前区间右端点的最大值
if (end == i) { // 当前区间合并完毕
ans.push_back(end - start + 1); // 区间长度加入答案
start = i + 1; // 下一个区间的左端点
}
}
return ans;
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 s 的长度。
- 空间复杂度:O(∣Σ∣)。其中 ∣Σ∣ 是字符集合的大小,本题字符均为小写字母,所以 ∣Σ∣=26。

浙公网安备 33010602011771号