贪心算法

贪心算法

贪心算法思路

什么时候用贪心?

感觉局部最优可以推出整体最优的时候,并且想不到反例的时候就可以用。其实

贪心问题的一般性解题步骤:

  1. 将问题分解成若干个个子问题。
  2. 找出合适的贪心策略。
  3. 求解每一个问题的最优解。
  4. 将局部问题最优解堆叠成全局最优解。

455. 分发饼干

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

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

贪心思路:

  1. 局部最优解是大饼干喂给胃口大的,全局最优解是饼干喂给尽可能多的小孩。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int findContentChildren(int[] g, int[] s) {
//对饼干和胃口值排序
Arrays.sort(s);
Arrays.sort(g);
//index表示孩子,count表示满足的孩子数量
int index = 0;
int count = 0;
//在不超过饼干数量和孩子数量的情况下遍历饼干
for(int i = 0; i < s.length && index < g.length; i++){
//当饼干能满足孩子,满足孩子的数量加一,递进下一个孩子
if(s[i] >= g[index]){
count++;
index++;
}
}
return count;
}
}

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

image-20231217211825353

局部最优:删除连续单调坡度上的节点。

整体最优:删除很多个波峰之间的单调坡度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
//当前差值
int curDiff = 0;
//上一个差值
int preDiff = 0;
int count = 1;
for (int i = 1; i < nums.length; i++) {
//得到当前差值
curDiff = nums[i] - nums[i - 1];
//如果当前差值和上一个差值为一正一负
//等于0的情况表示初始时的preDiff
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
count++;
preDiff = curDiff;
}
}
return count;
}
}

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

局部最优:当前连续和为负数的时候立刻放弃,从下一个元素重新计算。

全局最优:记录数组的最大和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public int maxSubArray(int[] nums) {
//思路:遍历数组,求和,该和可能增值减减,但连续和为负值就说明它不可用,就弃用这段序列
int currentCount = 0;
int maxCount = 0;
boolean flag = true;
//maxCount要初始化为最大负数值
for(int i = 0; i < nums.length; i++){
maxCount = maxCount > nums[i] ? nums[i] : maxCount;
}
for(int i = 0; i < nums.length; i++){
if(nums[i] > 0){
flag = false;
}
currentCount += nums[i];
currentCount = currentCount <= 0 ? 0 : currentCount;
maxCount = currentCount > maxCount ? currentCount : maxCount;
}
//这里加一个逻辑,如果全是负数,就取最大的负数
if(flag){
maxCount = nums[0];
for(int i = 0; i < nums.length; i++){
maxCount = -nums[i] > -maxCount ? maxCount : nums[i];
}
}
return maxCount;

}
}

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

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

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

返回 你能获得的 最大 利润

局部最优:每天利润最大。

整体最优: 所有的利润最大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int maxProfit(int[] prices) {
//这题用贪心的思路,如果后一天的价格比前一天大,就可以买。因为可以在同一天买入卖出。
int maxProfit = 0;
int currentProfit = 0;
if(prices.length == 1){
return 0;
}
for(int i = 1; i < prices.length; i++){
currentProfit = prices[i] - prices[i-1];
if(currentProfit > 0){
maxProfit += currentProfit;
}
}
return maxProfit;
}
}

55. 跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

思路:关键不在于跳到哪里,而在于最大跳跃步数,也就是最大覆盖范围能否到终点。

局部最优:每次跳跃取最大覆盖范围。

整体最优:最后得到整体最大范围,看看能否到终点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public boolean canJump(int[] nums) {
if (nums.length == 1) {
return true;
}
//覆盖范围, 初始覆盖范围应该是0,因为下面的迭代是从下标0开始的
int coverRange = 0;
//在覆盖范围内更新最大的覆盖范围
for (int i = 0; i <= coverRange; i++) {
coverRange = Math.max(coverRange, i + nums[i]);
if (coverRange >= nums.length - 1) {
return true;
}
}
return false;
}
}

45. 跳跃游戏 II

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

局部最优:每一步的覆盖范围最大。

全局最优:全局的覆盖范围最大,然后记录步数就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int jump(int[] nums) {
int result = 0;
// 当前覆盖的最远距离下标
int end = 0;
// 下一步覆盖的最远距离下标
int temp = 0;
for (int i = 0; i <= end && end < nums.length - 1; ++i) {
temp = Math.max(temp, i + nums[i]);
// 可达位置的改变次数就是跳跃次数
if (i == end) {
end = temp;
result++;
}
}
return result;
}
}

1005. K 次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]

重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

局部最优:将负数转变为正数。

​ 在已经是正数的情况下,转换最小的正整数。

整体最优:将所有负数转变为正数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
//取反的逻辑是
//1. 找最小的取反,只取反一次
//2. 如果剩下都大于0,那么就重排序,取反第一个多次
int sum = 0;
int count = k;
Arrays.sort(nums);
for(int i = 0; i < k && i < nums.length; i++){
if(nums[i] >= 0){
break;
}
nums[i] = -nums[i];
count -= 1;
}
Arrays.sort(nums);
for(int i = 0; i < count; i++){
nums[0] = -nums[0];
}
for (int num : nums) {
sum += num;
}
return sum;

}
}

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

思路:这题其实是把cos和gas数组转化为剩余油量的数组,并且要清楚从0出发到 i 剩余油量小于0,说明中间任何一个点都不是出发点,起始位置一定要从i + 1算起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int index = 0;
for (int i = 0; i < gas.length; i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if (curSum < 0) {
index = (i + 1) % gas.length ;
curSum = 0;
}
}
if (totalSum < 0) return -1;
return index;
}
}

135. 分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

思路:这种复杂的相邻问题,不妨用双向贪心来解!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
/**
分两个阶段
1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1
2、起点下标 ratings.length - 2 从右往左, 只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,这样才符合 它比它左边的大,也比它右边大
*/
public int candy(int[] ratings) {
int len = ratings.length;
int[] candyVec = new int[len];
candyVec[0] = 1;
for (int i = 1; i < len; i++) {
candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1;
}

for (int i = len - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
}
}

int ans = 0;
for (int num : candyVec) {
ans += num;
}
return ans;
}
}

860. 柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public boolean lemonadeChange(int[] bills) {
int[] arr = new int [2];
for(int i = 0 ; i < bills.length; i++){
if(bills[i] == 5){
++arr[0];
}
if(bills[i] == 10){
--arr[0];
++arr[1];
}
if(bills[i] == 20){
--arr[0];
--arr[1];
if(arr[1] < 0){
++arr[1];
--arr[0];
--arr[0];
}
}
if(arr[0] < 0 || arr[1] < 0){
return false;
}
}
return true;

}
}

406. 根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

难点:

  1. java中Arrays.sort运用不熟练
  2. 想不到双向贪心
  3. 队列化数组不熟练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排(身高相同k小的站前面)
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
});

LinkedList<int[]> que = new LinkedList<>();

for (int[] p : people) {
que.add(p[1],p); //Linkedlist.add(index, value),會將value插入到指定index裡。
}

return que.toArray(new int[people.length][]);
}
}

452. 用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

难点:

  1. 知道要贪最大重叠气球,但不知道用代码怎么表示。
  2. 遍历的是气球而不是气球的总长度,因为没必要,比较前一个气球的右边界是否大于等于后一个气球的左边界即可,如果重叠,就把重叠部分看成是一个气球,更新右边界。只有前一个气球的右边界和后一个气球的左边界不等时才箭数加一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int findMinArrowShots(int[][] points) {
// 根据气球直径的开始坐标从小到大排序
// 使用Integer内置比较方法,不会溢出
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));

int count = 1; // points 不为空至少需要一支箭
for (int i = 1; i < points.length; i++) {
if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>=
count++; // 需要一支箭
} else { // 气球i和气球i-1挨着
points[i][1] = Math.min(points[i][1], points[i - 1][1]); // 更新重叠气球最小右边界
}
}
return count;
}
}

435. 无重叠区间

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

难点:

  1. 这题和射气球很像,知道要排序,但不知道怎么求要移除哪些区间。下意识就开始模拟。

  2. 其实不需要求区间,求移除区间的数量就行,也就是求有几个不重叠的区段。

    image-20231212234817828

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
//先排序
Arrays.sort(intervals, (a,b)-> {
return Integer.compare(a[0],b[0]);
});
int count = 1;
for(int i = 1;i < intervals.length;i++){
if(intervals[i][0] < intervals[i-1][1]){
//更新重叠右边界
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
continue;
}else{
count++;
}
}
return intervals.length - count;
}
}

763. 划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

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

难点:

  1. 同一字母最多出现在一个片段中,这句话难理解,其实是在区间尽可能多的情况下,一个字母不能出现在多个区间。
  2. 那么如何去实现呢?先创建一个数组记录各个字母的最远位置,那么由左向右遍历的过程中,不断寻找该区间内字母最远出现的点(也就是最大值),如果找到字符恰好就是该字符并且就在最远那个点,那么就记录长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public List<Integer> partitionLabels(String S) {
List<Integer> list = new LinkedList<>();
int[] edge = new int[26];
char[] chars = S.toCharArray();
//更新字母数组中,对应字母存在的最远下标
for (int i = 0; i < chars.length; i++) {
edge[chars[i] - 'a'] = i;
}
int idx = 0;
int last = -1;
for (int i = 0; i < chars.length; i++) {
//找到字符出现的最远边界
idx = Math.max(idx,edge[chars[i] - 'a']);
if (i == idx) {
list.add(i - last);
last = i;
}
}
return list;
}
}

56. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

难点:

  1. 这题其实很直观,排序后不断更新最大右边界,直到下一个数组的左边界大于最大右边界。

  2. 主要欠缺在细节上:用什么容器去储存结果?结果最后如何转成数组形式?如何模拟更新左右边界?

    注意:下一次可以先if比较简单的场景,else较难的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new LinkedList<>();
//按照左边界排序
Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));
//initial start 是最小左边界
int start = intervals[0][0];
int rightmostRightBound = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
//如果左边界大于最大右边界
if (intervals[i][0] > rightmostRightBound) {
//加入区间 并且更新start
res.add(new int[]{start, rightmostRightBound});
start = intervals[i][0];
rightmostRightBound = intervals[i][1];
} else {
//更新最大右边界
rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1]);
}
}
res.add(new int[]{start, rightmostRightBound});
return res.toArray(new int[res.size()][]);
}
}

738. 单调递增的数字

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

难点:

  1. 这题思路清晰,就是从后往前贪大的数字,难点在于比较每个数字大小和整体大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int monotoneIncreasingDigits(int n) {
//先转成字符串,然后再转换成数组
String s = String.valueOf(n);
char[] chars = s.toCharArray();
int start = s.length();
//从右往前遍历,如果后一个大于前一个就使得前一个减一,后一个变为9
for (int i = s.length() - 2; i >= 0; i--) {
if (chars[i] > chars[i + 1]) {
chars[i]--;
start = i+1;
}
}
for (int i = start; i < s.length(); i++) {
chars[i] = '9';
}
return Integer.parseInt(String.valueOf(chars));
}
}

968. 监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

难点:

  1. 二叉树的题好久没做了,已经忘了树的语法。
  2. 想不到用后续遍历方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
int res=0;
public int minCameraCover(TreeNode root) {
// 对根节点的状态做检验,防止根节点是无覆盖状态 .
if(minCame(root)==0){
res++;
}
return res;
}
/**
节点的状态值:
0 表示无覆盖
1 表示 有摄像头
2 表示有覆盖
后序遍历,根据左右节点的情况,来判读 自己的状态
*/
public int minCame(TreeNode root){
if(root==null){
// 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
return 2;
}
int left=minCame(root.left);
int right=minCame(root.right);

// 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
if(left==2&&right==2){
//(2,2)
return 0;
}else if(left==0||right==0){
// 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头
// (0,0) (0,1) (0,2) (1,0) (2,0)
// 状态值为 1 摄像头数 ++;
res++;
return 1;
}else{
// 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头,
// 那么本节点就是处于被覆盖状态
return 2;
}
}
}

贪心算法
https://wengerblogs.me/2025/05/13/贪心算法/
Author
Wenger
Posted on
May 13, 2025
Licensed under