系列文章目录
方法论
Hot100 之贪心算法
121. 买卖股票的最佳时机
121. 买卖股票的最佳时机:给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 =6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
分析:
根据数组 prices
,我们可以知道每一天的价格。所以,我们可以再一次遍历中,记录直到当天的历史最低的价格,然后计算当前股价与历史最低的差值,记录每一天的收益情况,最后保留最优值。
答案:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int minPrice = INT32_MAX, maxProfit = 0;
// 遍历每一天,先记录历史最低的价格,再计算当前股价与历史最低的差值,来取出最优值
for(int i = 0; i < prices.size(); ++i)
{
minPrice = min(minPrice, prices[i]); // 先记录历史最低
maxProfit = max(maxProfit, prices[i] - minPrice); // 计算当前股价与历史最低的差值
}
return maxProfit;
}
};
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 ,所以永远不可能到达最后一个下标。
分析:
先看示例 2,nums=[3,2,1,0,4]
。我们在遍历数组的同时,维护最右可以到达的位置 mx
,如下表:
i | nums[i] | i+nums[i] | max |
---|---|---|---|
0 | 3 | 3 | 3 |
1 | 2 | 3 | 3 |
2 | 1 | 3 | 3 |
3 | 0 | 3 | 3 |
4 | 4 | 8 | 失败 |
从 0 可以跳到 1,2,3,但是无法从 1,2,3 中的任何位置跳到 4。
当我们遍历到 i=4 时,发现 i > m x i>mx i>mx,这意味着当前 i
是无法到达的,返回 false
。
然后来看示例 1,nums=[2,3,1,1,4]
,在遍历数组的同时,维护最远可以到达的位置 mx
,如下表:
i | nums[i] | i+nums[i] | max |
---|---|---|---|
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,3,4,mx 更新成 4。
从 2 可以跳到 3,mx 不变,任为 4。
从 3 可以跳到 4,mx 不变,任为 4。
到达 4,返回 true。
一般地,算法如下:
(1)从左到右遍历 nums
,同时维护能跳到的最远位置 mx
,初始值为 0
。
(2)如果 i>mx
,说明无法跳到 i
,返回 false
。
(3)否则,用 i+nums[i]更新 mx 的最大值。
(4)如果循环中没有返回 false,那么最后返回 true。
答案:
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
45. 跳跃游戏 II:给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 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
提示:
(1) 1 <= nums.length <= 104
(2) 0 <= nums[i] <= 1000
(3) 题目保证可以到达 n - 1
分析:
题目保证可以到达 n - 1,所以问题只要思考最小跳跃数就行。
注意:不是在无路可走的那个位置i
,使用nums[i]
造桥,而是当发现无路可走的时候,时光倒流到能跳到最远点的那个位置造桥。换句话说,在无路可走之前,我们只是在默默地收集信息,没有实际造桥。当发现无路可走的时候,才从收集到的信息中,选择最远点造桥。**因此,维持一个变量,记录下一座要造桥的右端点的最大值。**所建造的这座桥的左端点(起跳位置)在我们当前走的这座桥的中间,而不是桥的末尾。
答疑,问:为什么代码只需要遍历到 n−2?
答:代码中的 for 循环计算的是在到达终点之前需要造多少座桥。n−1 已经是终点了,不需要造桥。
答案:
class Solution {
public:
int jump(vector<int>& nums) {
int ans = 0;
int cur_right = 0; // 已建造的桥的右端点
int next_right = 0; // 下一座桥的右端点的最大值
for(int i = 0; i < nums.size()-1; ++i)
{
next_right = max(next_right, i+nums[i]); // 记录下一座桥的最远点
if(i == cur_right) // 无路可走,必须建桥
{
cur_right = next_right; // 建桥后,最远可以到达 next_right
ans ++;
}
}
return ans;
}
};
763. 划分字母区间
763. 划分字母区间:给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc"
能够被分为 ["abab", "cc"]
,但类似 ["aba", "bcc"]
或 ["ab", "ab", "cc"]
的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释: 划分结果为"ababcbaca"、“defegde”、“hijhklij” 。 每个字母最多出现在一个片段中。 像"ababcbacadefegde", “hijhklij” 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = “eccbbbbdec”
输出:[10]
分析:
「同一字母最多出现在一个片段中」意味着,一个片段若要包含字母 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
作为下一个区间的左端点。 - 遍历完毕,返回答案。
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;
}
};