Update Leetcode 题解 - 贪心思想.md

更新贪心策略证明与比较器说明,补充身高重建队列、划分字母区间等题目的贪心思路与推导演示,文末新增参考资料。
This commit is contained in:
CyC2018
2026-10-30 20:18:40 +08:00
parent aced35b435
commit ed12ee8d52
+38 -18
View File
@@ -12,6 +12,7 @@
* [9. 修改一个数成为非递减数组](#9-修改一个数成为非递减数组)
* [10. 子数组最大的和](#10-子数组最大的和)
* [11. 分隔字符串使同种字符出现在一起](#11-分隔字符串使同种字符出现在一起)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -33,9 +34,12 @@ Output: 2
1. 给一个孩子的饼干应当尽量小并且又能满足该孩子,这样大饼干才能拿来给满足度比较大的孩子。
2. 因为满足度最小的孩子最容易得到满足,所以先满足满足度最小的孩子。
在以上的解法中,只在每次分配饼干时选择一种看起来是当前最优的分配方法,但无法保证这种局部最优的分配方法最后能得到全局最优解。可以用反证法证明该贪心策略能得到全局最优解:假设存在一种比该贪心策略更优的最优策略。如果不存在这种最优策略,表示贪心策略就是最优策略,得到的解也就是全局最优解。
局部最优的选择不一定得到全局最优,需要证明。可以用交换论证:设某个最优方案给满足度最小的孩子分配了饼干 n,贪心选的是能满足这个孩子的最小饼干 m,m 不大于 n。
证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,可以给该孩子分配第 n 个饼干,并且 m \< n。经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。
- 如果 m 没有被最优方案使用,把 n 换成 m,满足的孩子数不变;
- 如果 m 分给了另一个孩子,两人交换饼干,那个孩子拿到的 n 不小于 m,仍然能被满足。
交换后的方案仍然最优,并且第一步与贪心相同。对剩下的孩子和饼干重复这个过程,最优方案可以逐步变成贪心方案,因此贪心方案也是最优的。
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e69537d2-a016-4676-b169-9ea17eeb9037.gif" width="430px"> </div><br>
@@ -104,18 +108,7 @@ public int eraseOverlapIntervals(int[][] intervals) {
}
```
使用 lambda 表达式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句:
```java
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return (o1[1] < o2[1]) ? -1 : ((o1[1] == o2[1]) ? 0 : 1);
}
});
```
实现 compare() 函数时避免使用 `return o1[1] - o2[1];` 这种减法操作,防止溢出。
需要注意的是,比较器用 Comparator.comparingInt 或 Integer.compare,避免写成 `o1[1] - o2[1]`:例如下一题的坐标可以取到 -2<sup>31</sup>,减去正数就会溢出。
## 3. 投飞镖刺破气球
@@ -133,7 +126,7 @@ Output:
题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都被刺破。求解最小的投飞镖次数使所有气球都被刺破。
也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。
也是计算不重叠的区间个数,不过和[不重叠的区间个数](#2-不重叠的区间个数)的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间,一支飞镖投在 2 上可以同时刺破它们。
```java
public int findMinArrowShots(int[][] points) {
@@ -167,11 +160,24 @@ Output:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
```
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比这个学生高或者和这个学生一样高。
为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。
身高 h 降序、个数 k 值升序,然后将某个学生插入队列的第 k 个位置中。
身高 h 降序、个数 k 值升序,然后将某个学生插入队列的第 k 个位置中。身高相同时 k 小的先插:同样高的人也计入 k,k 大的人后插,前面才有足够的人。以输入为例,排序后依次插入:
```text
排序后:[7,0] [7,1] [6,1] [5,0] [5,2] [4,4]
插入 [7,0] 到位置 0:[7,0]
插入 [7,1] 到位置 1:[7,0] [7,1]
插入 [6,1] 到位置 1:[7,0] [6,1] [7,1]
插入 [5,0] 到位置 0:[5,0] [7,0] [6,1] [7,1]
插入 [5,2] 到位置 2:[5,0] [7,0] [5,2] [6,1] [7,1]
插入 [4,4] 到位置 4:[5,0] [7,0] [5,2] [6,1] [4,4] [7,1]
```
插入 [5,2] 时,队列中的人都不比 5 矮,放在位置 2 就正好有 2 个人在前面;之后插入的人都更矮,不计入它的 k。
```java
public int[][] reconstructQueue(int[][] people) {
@@ -248,6 +254,8 @@ Output: True
题目描述:flowerbed 数组中 1 表示已经种下了花朵。花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。
从左到右能种就种。尽早种不会让结果变差:种在这里只挡住右边一个位置,推迟到更右的空位再种,留给后面的位置只会更少。
```java
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int len = flowerbed.length;
@@ -278,6 +286,8 @@ s = "abc", t = "ahbgdc"
Return true.
```
对 s 的每个字符,在 t 中上一个匹配位置之后找最早出现的位置,给后面的字符留下的空间最大。
```java
public boolean isSubsequence(String s, String t) {
int index = -1;
@@ -305,7 +315,9 @@ Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
题目描述:判断一个数组是否能只修改一个数就成为非递减数组。
在出现 nums[i] \< nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] \< nums[i - 2],修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。
在出现 nums[i] \< nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且不影响后续的操作。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。
还有一个比较特别的情况就是 nums[i] \< nums[i - 2],修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。例如 [3, 4, 2, 5] 中 2 \< 3,把 4 改为 2 得到 [3, 2, 2, 5] 仍然下降,只能把 2 改为 4。
```java
public boolean checkPossibility(int[] nums) {
@@ -338,6 +350,8 @@ For example, given the array [-2,1,-3,4,-1,2,1,-5,4],
the contiguous subarray [4,-1,2,1] has the largest sum = 6.
```
preSum 表示以当前元素结尾的子数组的最大和。如果前面的 preSum 不大于 0,接上它不会让和变大,不如从当前元素重新开始。
```java
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
@@ -368,6 +382,8 @@ This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.
```
先记录每个字符最后出现的位置。段内每出现一个字符,这一段至少要延伸到它最后出现的位置,因此用它更新 lastIndex;扫描到 lastIndex 时段内字符都不再出现,在这里切开就是最短的一段。例如第二段从 d 开始,d 最后出现在 14,段内的 e 最后出现在 15,于是延伸到 15,长度为 7。
```java
public List<Integer> partitionLabels(String S) {
int[] lastIndexsOfChar = new int[26];
@@ -394,3 +410,7 @@ private int char2Index(char c) {
return c - 'a';
}
```
## 参考资料
- [LeetCode 题库](https://leetcode.com/problemset/)