- 贪心可真费脑子,简直就是脑经急转弯
九、贪心
9.1贪心基本概念
-
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
-
一般解题步骤,代码随想录
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
9.2简单题
-
分发饼干(455)
class Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(s); Arrays.sort(g); int i = 0; for (int x : s) { if (i < g.length && x >= g[i]) { i++; } } return i; } }
灵神的写法太妙了。第一眼看到的想到的写法排序+双指针。先对两个数组排序遍历食物数组(遍历的迭代遍历也可以当成指针),然后依次满足小孩数组(通过一个指针),如果大于等于小孩数组对应元素的值就位移小孩数组那个指针。再维护一个count变量来记录大小
这个时候灵神的写法,小孩数组的指针位移量起始就是count值,直接返回即可。
-
K次取反后最大化的数组和
很简单,先排序,反转最小的树,再累加。本题因为数据少可以直接用桶排序更优
class Solution { public int largestSumAfterKNegations(int[] nums, int k) { Map<Integer, Integer> freq = new HashMap<Integer, Integer>(); for (int num : nums) { freq.put(num, freq.getOrDefault(num, 0) + 1); } int ans = Arrays.stream(nums).sum(); for (int i = -100; i < 0; ++i) { if (freq.containsKey(i)) { int ops = Math.min(k, freq.get(i)); ans += (-i) * ops * 2; freq.put(i, freq.get(i) - ops); freq.put(-i, freq.getOrDefault(-i, 0) + ops); k -= ops; if (k == 0) { break; } } } if (k > 0 && k % 2 == 1 && !freq.containsKey(0)) { for (int i = 1; i <= 100; ++i) { if (freq.containsKey(i)) { ans -= i * 2; break; } } } return ans; } }
-
柠檬水找零(力扣860)
很简单,只要搞清楚逻辑,局部最优就是,给10找5,给20,有10找10,没10找5
class Solution { public boolean lemonadeChange(int[] bills) { int five = 0; int ten = 0; for (int i = 0; i < bills.length; ++i) { if (bills[i] == 5) { five++; } else if (bills[i] == 10) { ten++; five--; } else if (ten > 0) { // 此时 b=20,返还 10+5 five--; ten--; } else { // 此时 b=20,返还 5+5+5 five -= 3; } if (five < 0) { return false; } } return true; } }
9.3中低档提
-
最大数组和(力扣53)
其实只要知道结论:如果前面的和是负数,那么从分界点重新计算,因为负数一定会拖累
class Solution { public int maxSubArray(int[] nums) { int temp = 0, res = Integer.MIN_VALUE; for (int i = 0; i < nums.length; ++i) { if (temp < 0) { temp = 0; } temp += nums[i]; res = Math.max(res, temp); } return res; } }
-
加油站(力扣134)
使用灵神的思路
不难发现先算好每个地方的加油和消耗油的差值,如果加油大于耗油那么一定有一条线路可以走完,而且是在最低点的下一点。而且最小点之前累加之和为非正数,最小值后一位累加为负数。所以结果就是最小的累加数的后一位。
注意值不可能为n,然后为n的话,肯定返回-1;
class Solution { public int canCompleteCircuit(int[] gas, int[] cost) { int ans = 0; int minS = 0; int s = 0; for (int i = 0; i < gas.length; i++) { s += gas[i] - cost[i]; if (s < minS) { minS = s; ans = i + 1; // 注意 s 减去 cost[i] 之后,汽车在 i+1 而不是 i } } return s < 0 ? -1 : ans; } }
-
根据身高重建队列(力扣406)
和分发糖果一样,注意一边一边处理,按照身高排序,如果相等,k小的优先。后面遍历移动的时候,因为前面一定大于后面,只要把后面移动到合适位置就可以了。这就是局部最优。贪心是真的费脑子
class Solution { public int[][] reconstructQueue(int[][] people) { Arrays.sort(people, (a, b) -> { if (a[0] == b[0]) return a[1] - b[1]; return b[0] - a[0]; }); List<int[]> que = new LinkedList<>(); for (int[] p : people) { que.add(p[1], p); } return que.toArray(new int[people.length][]); } }
9.4中档题
-
摆动序列(力扣376)
public class Solution { List<Integer> tempList = new ArrayList<>(); public int wiggleMaxLength(int[] nums) { if (nums.length < 2) { return nums.length; } for (int i = 1; i < nums.length; i++) { int temp = nums[i] - nums[i - 1]; // 差值为0直接跳过,不记录 if (temp == 0) { continue; } // 如果当前差值与上一个差值同号,则跳过(不记录) if (tempList.size() >= 1 && temp * tempList.get(tempList.size() - 1) >= 0) { continue; } // 记录当前有效的差值 tempList.add(temp); } // 返回摆动序列的长度,tempList 记录的是差值,因此长度为 tempList.size() + 1 return tempList.size() + 1; } }
其实很简单,画一个图。如果出现不连续的摆动序列,直接取索引为i-1作为摆动序列的值就可以,这个可以画图理解下
这种情况,只有取最新的i-1才为最优解
-
买卖股票的最佳实际II(力扣122)
可以发现,其实贪心难的是思路,写法还是很方便的
class Solution { public int maxProfit(int[] prices) { int res = 0; for (int i = 1; i < prices.length; ++i) { if (prices[i] - prices[i - 1] > 0) { res += (prices[i] - prices[i - 1]); } } return res; } }
只要算没相隔两天的差值,如果是正值那么就算,负的就不要。也找不到反例去反驳。
-
跳跃游戏(力扣55)
误区是不能执着于眺几步。我们要把每一个元素的跳跃步数理解一个覆盖区间。只是在不断更新覆盖区间,每一次覆盖区间中要累加确定的新的最大的覆盖区间就能得出最远能不能到最后的元素
class Solution { public boolean canJump(int[] nums) { // 确定覆盖范围 int cover = 0; // 如果数组长度为1,一定满足直接返回 if (nums.length == 1) return true; // 寻找最大覆盖范围 for (int i = 0; i <= cover; ++i) { cover = Math.max(i + nums[i], cover); if (cover >= nums.length - 1) return true; } return false; } }
-
跳跃游戏II(力扣45)
与前一题类似,相当于是在前一题的思路下把跳跃的次数算出来(这里采用灵神的代码思路,真的很棒)
class Solution { public int jump(int[] nums) { // 确定覆盖范围 int cover = 0; int temp = nums[0]; int res = 1; // 如果数组长度为1,一定满足直接返回 if (nums.length == 1) return 0; // 寻找最大覆盖范围 for (int i = 0; i < nums.length - 1; ++i) { cover = Math.max(i + nums[i], cover); if (i == temp) { temp = cover; res++; } } return res; } }
直接抽象的去想这题,会比较难写,我们比作为实际问题,没跳跃一次就相当于建一条桥,问建最少建多少个桥到目的地。我们如何确定一次跳跃呢,也就是旧桥确定再哪个地方建新桥的时候+1,就是一次跳跃。所以我们需要两个变量控制新桥和旧桥的控制范围。先来一个旧桥的图纸,(有了图纸,就一定会建,所以先res=1),然后就是确定新桥的图纸在哪,等到旧桥所有可能的地方都观察过了,确定合适的旧桥,旧桥就可以建完毕了。开始建新桥,循环下去。
这里要注意为什么for循环遍历到索引为n-2的元素,不是最后一个,是因为我们认为有了图纸就一定会建桥,具体建桥是发生在
if (i == temp)
这里的,我们到达了最后一个元素,就不需要建桥了,如果正好跳跃的元素最大值就是最后一个元素,就会导致再在原地建一次桥,使结果大于1。 -
分发糖果(力扣135)
直接上代码,不要想复杂,要分治处理,先处理右孩子大于左孩子,然后再处理左孩子大于右孩子。要注意右孩子大于左孩子,一定从左向右,要从头更新,要不然算出来的值没有价值。反之反过来
class Solution { public int candy(int[] ratings) { int[] candyAL = new int[ratings.length]; candyAL[0] = 1; // 右边比左边大,必须从左向右,并初始化 for (int i = 1; i < ratings.length; i++) { candyAL[i] = (ratings[i] > ratings[i - 1]) ? candyAL[i - 1] + 1 : 1; } // 左边比右边大,必须从右向左 for (int i = ratings.length - 2; i >= 0; i--) { if (ratings[i] > ratings[i + 1]) { candyAL[i] = Math.max(candyAL[i], candyAL[i + 1] + 1); } } int ans = 0; for (int num : candyAL) { ans += num; } return ans; } }