【Hot100】贪心算法

发布于:2025-08-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

系列文章目录

【Hot100】二分查找



方法论

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]这些区间满足「同一字母最多出现在一个片段中」的要求。由于题目要求划分出尽量多的片段,而我们又无法将上述区间的任何区间划分开,所以合并出的区间长度即为答案。(本质是合并区间)


答案:

  1. 遍历 s,计算字母 cs 中的最后出现的下标 last[c]
  2. 初始化当前正在合并的区间左右端点 start=0, end=0
  3. 再次遍历 s,由于当前区间必须包含所有 s[i],所以用 last[s[i]] 更新区间右端点 end 的最大值。
  4. 如果发现 end=i,那么当前区间合并完毕,把区间长度 end−start+1 加入答案。然后更新 start=i+1 作为下一个区间的左端点。
  5. 遍历完毕,返回答案。
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;
    }
};

网站公告

今日签到

点亮在社区的每一天
去签到