langShiftlangShift

贪心算法(题单顺序)

热题 100 - 贪心算法分组(按题单顺序):121 / 55 / 45 / 763

本页包含题单「贪心算法」分组的全部题目,顺序与 list.json 保持一致。

贪心题统一“自检清单”(点击展开)
  • 你在每一步做的局部选择是什么?(例如:当前最小买入价、当前能覆盖到的最远位置、当前段的最远右边界)
  • 为什么“局部最优 ⇒ 全局最优”? 常见证明方式:不变量、交换论证(exchange argument)、单调性剪枝。
  • 什么时候贪心不适用? 一旦局部选择会影响未来多种分支且无法用不变量锁死,往往要转 DP/图搜索。

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。

 

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104
最优解(一次遍历:维护历史最低价)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 这题就是在问:选一个买入日 i、一个卖出日 j(j>i),最大化 prices[j]-prices[i]
  • 朴素做法:枚举 i、j,(O(n^2))。
  • 关键推理:卖出当天的最佳买入价一定是“它之前出现过的最低价”
    • 你每天都可以把“到今天为止的最低价”记住:minPrice
    • 今天如果卖出,利润就是 prices[t] - minPrice
    • 一边扫一边更新最大利润即可。
  • 这就是贪心:每一天都做局部最优的“最低买入价更新”,最终得到全局最优利润。
类似题目(前缀最值:维护历史最小/最大)
minSoFar = +inf
for x in arr:
ans = max(ans, x - minSoFar)
minSoFar = min(minSoFar, x)

55. 跳跃游戏

力扣原题:55. 跳跃游戏
Medium
贪心
数组
动态规划

给你一个非负整数数组 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
最优解(贪心:维护能到达的最远下标)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:从 0 出发,每个位置 i 最多跳 nums[i] 步,问能不能到最后。
  • 关键推理:你不需要知道“怎么跳”,只需要知道“最远能覆盖到哪”
    • far 表示目前为止能到达的最远下标。
    • 扫描到位置 i 时:
      • 如果 i > far,说明你连 i 都到不了,后面更到不了 → false。
      • 否则你可以从 i 再扩展覆盖:far = max(far, i + nums[i])
    • 一旦 far 覆盖到末尾,就一定可达。
  • 这就是贪心:每一步都把“覆盖范围”扩到最大,不回头、不枚举路径。
类似题目(区间覆盖:维护 farthest reach)
for i:
if i > far: fail
far = max(far, i + step[i])

45. 跳跃游戏 II

力扣原题:45. 跳跃游戏 II
Medium
贪心
数组
动态规划

给定一个长度为 n0 索引整数数组 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 <= nums.length <= 104
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 n - 1
最优解(贪心分层:每层代表一次跳跃)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 和 55 的区别:55 只问能不能到,45 要最少跳几次。
  • 关键推理:把跳跃看成 BFS 的“层”(但用贪心实现):
    • 第 0 次跳能覆盖 [0..end]
    • 在这段覆盖范围里,你可以选择任意一个点作为落脚点,再跳一次。
    • 这一层里最好的选择,一定是让下一层覆盖的最远边界最大:farthest = max(i + nums[i])
  • 为什么 i==end 时 steps++?
    • 说明你已经扫描完了“当前这一跳能覆盖的所有位置”,必须进行下一跳了。
    • end 更新成 farthest,相当于进入下一层。
  • 这就是贪心:每一跳都把“下一跳能覆盖的最远边界”做到最大,保证跳数最少。
类似题目(分层贪心:currentEnd / farthest)
for i:
farthest = max(farthest, i+step[i])
if i==end: steps++; end=farthest

763. 划分字母区间

力扣原题:763. 划分字母区间
Medium
贪心
哈希表
双指针
字符串

给你一个字符串 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]

 

提示:

  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成
最优解(贪心:先记每个字符最后出现位置,再尽量扩区间)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:把字符串切成尽可能多段,每个字母只能出现在其中一段里。
  • 关键推理:一段的右边界必须至少覆盖这段里所有字符的“最后一次出现位置”
    • 先预处理 last[ch]:每个字符最后出现的下标。
    • 从左到右扫:
      • 当前字符 s[i] 的最后出现位置是 last[s[i]]
      • 当前段的右边界 end 必须至少到达它:end = max(end, last[s[i]])
    • i == end 时,说明从 start 到 end 之间出现过的所有字符,它们的最后出现位置都 ≤ end。
      • 换句话说:这段里的字符不会再出现在后面了,段可以安全切开。
  • 为什么这是“最多段”?
    • 一旦满足 i==end 就立刻切,贪心地尽早结束当前段,才能留下更多空间给后面的段。
类似题目(区间扩张:先预处理 last,再遇到字符扩 end)
end = max(end, last[s[i]])
if i==end: cut