Skip to content

贪心

贪心算法理论基础

贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

没了,这个只能靠感觉

1. 分发饼干

🔗力扣题目链接https://leetcode.cn/problems/assign-cookies/description/

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j]。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。

示例 1:

输入: g = [1,2,3], s = [1,1]

输出: 1

解释:

你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。

虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。

所以你应该输出 1。

示例 2:

输入: g = [1,2], s = [1,2,3]

输出: 2

解释:

你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。

你拥有的饼干数量和尺寸都足以让所有孩子满足。

所以你应该输出 2。

md
为了满足尽可能多的孩子,我们应该采用贪心策略:
1、将孩子的胃口值数组 `g` 和饼干尺寸数组 `s` 都进行升序排序
2、优先满足胃口小的孩子,这样可以保证整体能满足的孩子数量最多
3、使用双指针分别指向孩子和饼干,当饼干能满足孩子时,两个指针都向后移动,否则只移动饼干指针
js
/**
 * @param {number[]} g
 * @param {number[]} s
 * @return {number}
 */
var findContentChildren = function (g, s) {
  // 从小到大排序
  g.sort((a, b) => a - b)
  s.sort((a, b) => a - b)
  // 孩子指针
  let child = 0
  // 饼干指针
  let cookie = 0
  // 满足孩子数量
  let count = 0

  while (child < g.length && cookie < s.length) {
    // 如果当前饼干能满足当前孩子
    if (g[child] <= s[cookie]) {
      child++
      count++
    }
    // 无论是否满足,都要看下一个饼干
    cookie++
  }
  return count
}

2. 跳跃游戏

🔗力扣题目链接https://leetcode.cn/problems/jump-game/description/

给你一个非负整数数组 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 , 所以永远不可能到达最后一个下标。

md
这道题其实跳几步无所谓,关键在于可跳的覆盖范围!
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。
这个范围内,别管是怎么跳的,反正一定可以跳过来。
**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!**
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

过程如下图:
i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。
而 cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)。
如果 cover 大于等于了终点下标,直接 return true 就可以了。
js
/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function (nums) {
  let maxReach = 0 // 当前能到达的最远位置
  for (let i = 0; i < nums.length; i++) {
    // 如果当前位置不可达,直接返回 false
    // 如果当前的最大距离还没有i大(正常情况是i<=maxReach,因为i每次只走一步),那后面就不可能到最后了
    if (i > maxReach) {
      return false
    }
    // 更新当前下标能到达的最远位置
    maxReach = Math.max(maxReach, i + nums[i])
    // 如果已经可以到达最后一个位置,提前返回
    if (maxReach > nums.length) {
      return true
    }
  }
  return true
}

3. 跳跃游戏 II

🔗力扣题目链接https://leetcode.cn/problems/jump-game-ii/description/

给定一个长度为 n0 索引整数数组 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

c
本题的目标是用最少的跳跃次数到达数组的最后一个位置,使用贪心策略,每次尽可能地跳到最远的位置,这样可以保证跳跃次数最少。
具体实现时,维护三个变量:

- maxReach:当前能跳到的最远位置。
- end:当前这一跳能到达的最远位置。
- steps:跳跃次数。
- 遍历数组,更新 maxReach,当遍历到 end 时,说明需要进行下一次跳跃,此时更新 end 为 maxReach,并增加跳跃次数。

不需要具体知道每一步跳到哪,只需要知道每次跳跃能覆盖的范围边界,当走到边界时就必须再跳一次
if (i === end) {
    end = maxReach // 更新下一次跳跃能到达的最远位置
    steps++ // 增加跳跃次数
}

- 当 maxReach 大于等于最后一个位置时,返回当前的跳跃次数。
js
/**
 * @param {number[]} nums
 * @return {number}
 */
var jump = function (nums) {
  let maxReach = 0 // 当前能跳到的最远位置
  let end = 0 // 当前这一跳能到达的最远位置
  let steps = 0 // 跳跃次数

  for (let i = 0; i < nums.length - 1; i++) {
    // 更新当前能跳到的最远位置
    maxReach = Math.max(maxReach, i + nums[i])

    // 如果遍历到当前这一跳的最远位置,则需要进行下一次跳跃
    /**
     * 当我们遍历到 i === end 时,意味着我们已经走完了当前这次跳跃所能覆盖的所有位置,
     * 此时我们必须进行下一次跳跃,所以需要增加跳跃次数
     * (也就是当前走过的,也就是i步,和end持平,那么就该计算下一个跳跃了)
     */
    if (i === end) {
      end = maxReach // 更新下一次跳跃能到达的最远位置
      steps++ // 增加跳跃次数
    }
  }

  return steps
}

4. 无重叠区间

🔗力扣题目链接https://leetcode.cn/problems/non-overlapping-intervals/description/

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

注意 只在一点上接触的区间是不重叠的。例如 [1, 2][2, 3] 是不重叠的。

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: intervals = [ [1,2], [1,2], [1,2] ]

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

输入: intervals = [ [1,2], [2,3] ]

输出: 0

解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

md
1、按区间结束位置排序:按照区间的结束位置进行升序排序,这样可以优先选择结束位置早的区间,为后续区间留出更多空间。
2、从第一个区间开始,选择结束位置最早的区间
3、遍历后续区间,如果当前区间的起始位置不小于上一个选中区间的结束位置,则选择该区间

总区间数减去选择的区间数,就是需要移除的最小区间数。
js
/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function (intervals) {
  if (intervals.length === 0) return 0

  // 按区间结束位置升序排序
  intervals.sort((a, b) => a[1] - b[1])

  let count = 1 // 选中的区间数量,初始化为1(选择第一个区间)
  let end = intervals[0][1] // 上一个选中区间的结束位置

  for (let i = 1; i < intervals.length; i++) {
    // 如果当前区间起始位置大于上一个选中区间的结束位置,说明不重叠
    if (intervals[i][0] >= end) {
      count++
      end = intervals[i][1] // 更新结束位置
    }
  }

  // 需要移除的区间数 = 总区间数 - 选中区间数
  return intervals.length - count
}

5. 划分字母区间

🔗力扣题目链接https://leetcode.cn/problems/partition-labels/description/

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"]["ab", "ab", "cc"] 的划分是非法的。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 ss 仅由小写英文字母组成)。

返回一个表示每个字符串片段的长度的列表。

示例 1

输入:s = "ababcbacadefegdehijhklij"

输出:[9,7,8]

解释:

划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。

每个字母最多出现在一个片段中。

像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。

示例 2

输入:s = "eccbbbbdec"

输出:[10]

md
这道题需要先理清楚这个切割的过程,就拿:ababcbacadefegde 举例,
每个字母只能出现在一个字符串中,例如第一个字母 a,我们就需要找到他最后出现的位置,也就是下标 8 的位置,
如果在下标 0-8 的区间内,其余的字母也在这个范围,那么就可以分割一个字符串,
如果其余字符超过了这个区间,那么就扩大。

那么,如何去找到他们的最大区间呢?
我们可以利用哈希表,去记录字母出现的最远位置

如何去找切割点呢?
当我们 for 循环遍历的 i,等于我们的 end 结束位置时,说明这个区间已经结束,那么我们就可以进行切割了。
js
/**
 * @param {string} s
 * @return {number[]}
 */
var partitionLabels = function (s) {
  // 使用哈希表存储字符串中每个元素的最后出现的位置(也就是最远位置)
  let endPosition = {}
  for (let i = 0; i < s.length; i++) {
    endPosition[s[i]] = i
  }

  // 记录每个切割的字符串长度
  const res = []
  // 切割字符串的开始下标
  let start = 0
  // 切割字符串的结束下标
  let end = 0

  for (let i = 0; i < s.length; i++) {
    // 获取当前字符的最远下标
    let max_position = endPosition[s[i]]
    // 去更新当前切割字符串的结束下标
    end = Math.max(end, max_position)
    // 如果结束下标等于当前的i,标识当前切割的字符串已经结束
    if (end === i) {
      // 存放长度
      res.push(end - start + 1)
      // 更新起始值
      start = end + 1
    }
  }
  return res
}

6. 买卖股票的最佳时机 II

🔗力扣题目链接https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1

输入:prices = [7,1,5,3,6,4]

输出:7

解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7 。

示例 2

输入:prices = [1,2,3,4,5]

输出:4

解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 最大总利润为 4 。

示例 3

输入:prices = [7,6,4,3,1]

输出:0

解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。

TIP

这个题目在【动态规划】部分会单独再说,这里只举例一下如何用贪心算法解决。

md
这道题我们只需要去算出每天的利润,只去加为正的利润即可。
js
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let total = 0
  // i从1开始,0没有利润
  for (let i = 1; i < prices.length; i++) {
    const p = prices[i] - prices[i - 1]
    if (p > 0) {
      total += p
    }
  }
  return total
}