当前位置:   article > 正文

代码随想录Day14--贪心算法专题_什么时候能用贪心

什么时候能用贪心

贪心算法理论基础

贪心一般解题步骤: 参见《代码随想录——什么时候用贪心》

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。

做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。

说实话贪心算法并没有固定的套路

所以唯一的难点就是如何通过局部最优,推出整体最优。

那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?

不好意思,也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。

有同学问了如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。

一般数学证明有如下两种方法:

  • 数学归纳法
  • 反证法

面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了

举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但没必要。

虽然这个例子很极端,但可以表达这么个意思:刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心

例如刚刚举的拿钞票的例子,就是模拟一下每次拿做大的,最后就能拿到最多的钱,这还要数学证明的话,其实就不在算法面试的范围内了,可以看看专业的数学书籍!

所以这也是为什么很多同学通过(accept)了贪心的题目,但都不知道自己用了贪心算法,因为贪心有时候就是常识性的推导,所以会认为本应该就这么做!

那么刷题的时候什么时候真的需要数学推导呢?

例如这道题目:链表:环找到了,那入口呢? (opens new window),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。

 实例操练

455.分发饼干

力扣题目链接

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

对每个孩子 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.

提示:

  • 1 <= g.length <= 3 * 10^4
  • 0 <= s.length <= 3 * 10^4
  • 1 <= g[i], s[j] <= 2^31 - 1

代码如下:

  1. class Solution {
  2. // 思路1:优先考虑饼干,小饼干先喂饱小胃口
  3. public int findContentChildren(int[] g, int[] s) {
  4. Arrays.sort(g);
  5. Arrays.sort(s);
  6. int start = 0;
  7. int count = 0;
  8. for (int i = 0; i < s.length && start < g.length; i++) {
  9. if (s[i] >= g[start]) {
  10. start++;
  11. count++;
  12. }
  13. }
  14. return count;
  15. }
  16. }
  1. class Solution {
  2. // 思路2:优先考虑胃口,先喂饱大胃口
  3. public int findContentChildren(int[] g, int[] s) {
  4. Arrays.sort(g);
  5. Arrays.sort(s);
  6. int count = 0;
  7. int start = s.length - 1;
  8. // 遍历胃口
  9. for (int index = g.length - 1; index >= 0; index--) {
  10. if(start >= 0 && g[index] <= s[start]) {
  11. start--;
  12. count++;
  13. }
  14. }
  15. return count;
  16. }
  17. }

 

376. 摆动序列

力扣题目链接(opens new window)

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

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

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

  • 输入: [1,7,4,9,2,5]
  • 输出: 6
  • 解释: 整个序列均为摆动序列。

示例 2:

  • 输入: [1,17,5,10,13,15,10,5,16,8]
  • 输出: 7
  • 解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

  • 输入: [1,2,3,4,5,6,7,8,9]
  • 输出: 2

代码如下:

  1. class Solution {
  2. public int wiggleMaxLength(int[] nums) {
  3. if (nums.length <= 1) {
  4. return nums.length;
  5. }
  6. //当前差值
  7. int curDiff = 0;
  8. //上一个差值
  9. int preDiff = 0;
  10. int count = 1;
  11. for (int i = 1; i < nums.length; i++) {
  12. //得到当前差值
  13. curDiff = nums[i] - nums[i - 1];
  14. //如果当前差值和上一个差值为一正一负
  15. //等于0的情况表示初始时的preDiff
  16. if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
  17. count++;
  18. preDiff = curDiff;
  19. }
  20. }
  21. return count;
  22. }
  23. }
  1. // DP
  2. class Solution {
  3. public int wiggleMaxLength(int[] nums) {
  4. // 0 i 作为波峰的最大长度
  5. // 1 i 作为波谷的最大长度
  6. int dp[][] = new int[nums.length][2];
  7. dp[0][0] = dp[0][1] = 1;
  8. for (int i = 1; i < nums.length; i++){
  9. //i 自己可以成为波峰或者波谷
  10. dp[i][0] = dp[i][1] = 1;
  11. for (int j = 0; j < i; j++){
  12. if (nums[j] > nums[i]){
  13. // i 是波谷
  14. dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
  15. }
  16. if (nums[j] < nums[i]){
  17. // i 是波峰
  18. dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
  19. }
  20. }
  21. }
  22. return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
  23. }
  24. }

 

53. 最大子序和

力扣题目链接(opens new window)

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

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释:  连续子数组  [4,-1,2,1] 的和最大,为  6。

代码如下:

  1. class Solution {
  2. public int maxSubArray(int[] nums) {
  3. if (nums.length == 1){
  4. return nums[0];
  5. }
  6. int sum = Integer.MIN_VALUE;
  7. int count = 0;
  8. for (int i = 0; i < nums.length; i++){
  9. count += nums[i];
  10. sum = Math.max(sum, count); // 取区间累计的最大值(相当于不断确定最大子序终止位置)
  11. if (count <= 0){
  12. count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
  13. }
  14. }
  15. return sum;
  16. }
  17. }
  1. // DP 方法
  2. class Solution {
  3. public int maxSubArray(int[] nums) {
  4. int ans = Integer.MIN_VALUE;
  5. int[] dp = new int[nums.length];
  6. dp[0] = nums[0];
  7. ans = dp[0];
  8. for (int i = 1; i < nums.length; i++){
  9. dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
  10. ans = Math.max(dp[i], ans);
  11. }
  12. return ans;
  13. }
  14. }

 

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

力扣题目链接(opens new window)

给定一个数组,它的第  i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

示例 2:

  • 输入: [1,2,3,4,5]
  • 输出: 4
  • 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例  3:

  • 输入: [7,6,4,3,1]
  • 输出: 0
  • 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 10 ^ 4
  • 0 <= prices[i] <= 10 ^ 4

代码如下:

贪心:

  1. // 贪心思路
  2. class Solution {
  3. public int maxProfit(int[] prices) {
  4. int result = 0;
  5. for (int i = 1; i < prices.length; i++) {
  6. result += Math.max(prices[i] - prices[i - 1], 0);
  7. }
  8. return result;
  9. }
  10. }

动态规划

 

  1. class Solution { // 动态规划
  2. public int maxProfit(int[] prices) {
  3. // [天数][是否持有股票]
  4. int[][] dp = new int[prices.length][2];
  5. // base case
  6. dp[0][0] = 0;
  7. dp[0][1] = -prices[0];
  8. for (int i = 1; i < prices.length; i++) {
  9. // dp公式
  10. dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
  11. dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
  12. }
  13. return dp[prices.length - 1][0];
  14. }
  15. }

55. 跳跃游戏

力扣题目链接(opens new window)

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例  1:

  • 输入: [2,3,1,1,4]
  • 输出: true
  • 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

示例  2:

  • 输入: [3,2,1,0,4]
  • 输出: false
  • 解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

代码如下:

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

45.跳跃游戏 II

力扣题目链接(opens new window)

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

  • 输入: [2,3,1,1,4]
  • 输出: 2
  • 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳  1  步,然后跳  3  步到达数组的最后一个位置。

说明: 假设你总是可以到达数组的最后一个位置。

代码如下:

  1. // 版本一
  2. class Solution {
  3. public int jump(int[] nums) {
  4. if (nums == null || nums.length == 0 || nums.length == 1) {
  5. return 0;
  6. }
  7. //记录跳跃的次数
  8. int count=0;
  9. //当前的覆盖最大区域
  10. int curDistance = 0;
  11. //最大的覆盖区域
  12. int maxDistance = 0;
  13. for (int i = 0; i < nums.length; i++) {
  14. //在可覆盖区域内更新最大的覆盖区域
  15. maxDistance = Math.max(maxDistance,i+nums[i]);
  16. //说明当前一步,再跳一步就到达了末尾
  17. if (maxDistance>=nums.length-1){
  18. count++;
  19. break;
  20. }
  21. //走到当前覆盖的最大区域时,更新下一步可达的最大区域
  22. if (i==curDistance){
  23. curDistance = maxDistance;
  24. count++;
  25. }
  26. }
  27. return count;
  28. }
  29. }
  1. // 版本二
  2. class Solution {
  3. public int jump(int[] nums) {
  4. int result = 0;
  5. // 当前覆盖的最远距离下标
  6. int end = 0;
  7. // 下一步覆盖的最远距离下标
  8. int temp = 0;
  9. for (int i = 0; i <= end && end < nums.length - 1; ++i) {
  10. temp = Math.max(temp, i + nums[i]);
  11. // 可达位置的改变次数就是跳跃次数
  12. if (i == end) {
  13. end = temp;
  14. result++;
  15. }
  16. }
  17. return result;
  18. }
  19. }

 

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

力扣题目链接(opens new window)

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

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

示例 1:

  • 输入:A = [4,2,3], K = 1
  • 输出:5
  • 解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。

示例 2:

  • 输入:A = [3,-1,0,2], K = 3
  • 输出:6
  • 解释:选择索引 (1, 2, 2) ,然后 A 变为 [3,1,0,2]。

示例 3:

  • 输入:A = [2,-3,-1,5,-4], K = 2
  • 输出:13
  • 解释:选择索引 (1, 4) ,然后 A 变为 [2,3,-1,5,4]。

提示:

  • 1 <= A.length <= 10000
  • 1 <= K <= 10000
  • -100 <= A[i] <= 100

代码如下:

  1. class Solution {
  2. public int largestSumAfterKNegations(int[] nums, int K) {
  3. // 将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
  4. nums = IntStream.of(nums)
  5. .boxed()
  6. .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
  7. .mapToInt(Integer::intValue).toArray();
  8. int len = nums.length;
  9. for (int i = 0; i < len; i++) {
  10. //从前向后遍历,遇到负数将其变为正数,同时K--
  11. if (nums[i] < 0 && K > 0) {
  12. nums[i] = -nums[i];
  13. K--;
  14. }
  15. }
  16. // 如果K还大于0,那么反复转变数值最小的元素,将K用完
  17. if (K % 2 == 1) nums[len - 1] = -nums[len - 1];
  18. return Arrays.stream(nums).sum();
  19. }
  20. }

134. 加油站

力扣题目链接(opens new window)

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

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

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且长度相同。
  • 输入数组中的元素均为非负数。

示例 1: 输入:

  • gas = [1,2,3,4,5]
  • cost = [3,4,5,1,2]

输出: 3 解释:

  • 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
  • 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
  • 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
  • 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
  • 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
  • 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
  • 因此,3 可为起始索引。

示例 2: 输入:

  • gas = [2,3,4]

  • cost = [3,4,3]

  • 输出: -1

  • 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油。开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油。开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油。你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。因此,无论怎样,你都不可能绕环路行驶一周。

代码如下:

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

 

135. 分发糖果

力扣题目链接(opens new window)

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

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

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:

  • 输入: [1,0,2]
  • 输出: 5
  • 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:

  • 输入: [1,2,2]
  • 输出: 4
  • 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

代码如下:

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

860.柠檬水找零

力扣题目链接(opens new window)

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

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

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

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

  • 输入:[5,5,5,10,20]
  • 输出:true
  • 解释:
    • 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
    • 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
    • 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
    • 由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:

  • 输入:[5,5,10]
  • 输出:true

示例 3:

  • 输入:[10,10]
  • 输出:false

示例 4:

  • 输入:[5,5,10,10,20]
  • 输出:false
  • 解释:
    • 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
    • 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
    • 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
    • 由于不是每位顾客都得到了正确的找零,所以答案是 false。

提示:

  • 0 <= bills.length <= 10000
  • bills[i] 不是 5 就是 10 或是 20

代码如下:

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

406.根据身高重建队列

力扣题目链接(opens new window)

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

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

示例 1:

  • 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
  • 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
  • 解释:
    • 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
    • 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
    • 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
    • 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
    • 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:

  • 输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
  • 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

提示:

  • 1 <= people.length <= 2000
  • 0 <= hi <= 10^6
  • 0 <= ki < people.length

题目数据确保队列可以被重建

代码如下:

  1. class Solution {
  2. public int[][] reconstructQueue(int[][] people) {
  3. // 身高从大到小排(身高相同k小的站前面)
  4. Arrays.sort(people, (a, b) -> {
  5. if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
  6. return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
  7. });
  8. LinkedList<int[]> que = new LinkedList<>();
  9. for (int[] p : people) {
  10. que.add(p[1],p); //Linkedlist.add(index, value),會將value插入到指定index裡。
  11. }
  12. return que.toArray(new int[people.length][]);
  13. }
  14. }

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

力扣题目链接(opens new window)

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

示例 1:

  • 输入:points = [[10,16],[2,8],[1,6],[7,12]]
  • 输出:2
  • 解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

示例 2:

  • 输入:points = [[1,2],[3,4],[5,6],[7,8]]
  • 输出:4

示例 3:

  • 输入:points = [[1,2],[2,3],[3,4],[4,5]]
  • 输出:2

示例 4:

  • 输入:points = [[1,2]]
  • 输出:1

示例 5:

  • 输入:points = [[2,3],[2,3]]
  • 输出:1

提示:

  • 0 <= points.length <= 10^4
  • points[i].length == 2
  • -2^31 <= xstart < xend <= 2^31 - 1

代码如下:

  1. /**
  2. * 时间复杂度 : O(NlogN) 排序需要 O(NlogN) 的复杂度
  3. * 空间复杂度 : O(logN) java所使用的内置函数用的是快速排序需要 logN 的空间
  4. */
  5. class Solution {
  6. public int findMinArrowShots(int[][] points) {
  7. // 根据气球直径的开始坐标从小到大排序
  8. // 使用Integer内置比较方法,不会溢出
  9. Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
  10. int count = 1; // points 不为空至少需要一支箭
  11. for (int i = 1; i < points.length; i++) {
  12. if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>=
  13. count++; // 需要一支箭
  14. } else { // 气球i和气球i-1挨着
  15. points[i][1] = Math.min(points[i][1], points[i - 1][1]); // 更新重叠气球最小右边界
  16. }
  17. }
  18. return count;
  19. }
  20. }

435. 无重叠区间

力扣题目链接(opens new window)

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

  • 输入: [ [1,2], [2,3], [3,4], [1,3] ]
  • 输出: 1
  • 解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

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

示例 3:

  • 输入: [ [1,2], [2,3] ]
  • 输出: 0
  • 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

代码如下:

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

方法二:按左边排序,不管右边顺序。相交的时候取最小的右边。

  1. class Solution {
  2. public int eraseOverlapIntervals(int[][] intervals) {
  3. Arrays.sort(intervals, (a,b)-> {
  4. return Integer.compare(a[0],b[0]);
  5. });
  6. int remove = 0;
  7. int pre = intervals[0][1];
  8. for(int i = 1; i < intervals.length; i++) {
  9. if(pre > intervals[i][0]) {
  10. remove++;
  11. pre = Math.min(pre, intervals[i][1]);
  12. }
  13. else pre = intervals[i][1];
  14. }
  15. return remove;
  16. }
  17. }

 

763.划分字母区间

力扣题目链接(opens new window)

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

  • 输入:S = "ababcbacadefegdehijhklij"
  • 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

提示:

  • S的长度在[1, 500]之间。
  • S只包含小写字母 'a' 到 'z' 。

代码如下:

  1. class Solution {
  2. public List<Integer> partitionLabels(String S) {
  3. List<Integer> list = new LinkedList<>();
  4. int[] edge = new int[26];
  5. char[] chars = S.toCharArray();
  6. for (int i = 0; i < chars.length; i++) {
  7. edge[chars[i] - 'a'] = i;
  8. }
  9. int idx = 0;
  10. int last = -1;
  11. for (int i = 0; i < chars.length; i++) {
  12. idx = Math.max(idx,edge[chars[i] - 'a']);
  13. if (i == idx) {
  14. list.add(i - last);
  15. last = i;
  16. }
  17. }
  18. return list;
  19. }
  20. }
  21. class Solution{
  22. /*解法二: 上述c++补充思路的Java代码实现*/
  23. public int[][] findPartitions(String s) {
  24. List<Integer> temp = new ArrayList<>();
  25. int[][] hash = new int[26][2];//26个字母2列 表示该字母对应的区间
  26. for (int i = 0; i < s.length(); i++) {
  27. //更新字符c对应的位置i
  28. char c = s.charAt(i);
  29. if (hash[c - 'a'][0] == 0) hash[c - 'a'][0] = i;
  30. hash[c - 'a'][1] = i;
  31. //第一个元素区别对待一下
  32. hash[s.charAt(0) - 'a'][0] = 0;
  33. }
  34. List<List<Integer>> h = new LinkedList<>();
  35. //组装区间
  36. for (int i = 0; i < 26; i++) {
  37. //if (hash[i][0] != hash[i][1]) {
  38. temp.clear();
  39. temp.add(hash[i][0]);
  40. temp.add(hash[i][1]);
  41. //System.out.println(temp);
  42. h.add(new ArrayList<>(temp));
  43. // }
  44. }
  45. // System.out.println(h);
  46. // System.out.println(h.size());
  47. int[][] res = new int[h.size()][2];
  48. for (int i = 0; i < h.size(); i++) {
  49. List<Integer> list = h.get(i);
  50. res[i][0] = list.get(0);
  51. res[i][1] = list.get(1);
  52. }
  53. return res;
  54. }
  55. public List<Integer> partitionLabels(String s) {
  56. int[][] partitions = findPartitions(s);
  57. List<Integer> res = new ArrayList<>();
  58. Arrays.sort(partitions, (o1, o2) -> Integer.compare(o1[0], o2[0]));
  59. int right = partitions[0][1];
  60. int left = 0;
  61. for (int i = 0; i < partitions.length; i++) {
  62. if (partitions[i][0] > right) {
  63. //左边界大于右边界即可纪委一次分割
  64. res.add(right - left + 1);
  65. left = partitions[i][0];
  66. }
  67. right = Math.max(right, partitions[i][1]);
  68. }
  69. //最右端
  70. res.add(right - left + 1);
  71. return res;
  72. }
  73. }

56. 合并区间

力扣题目链接(opens new window)

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

  • 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
  • 输出: [[1,6],[8,10],[15,18]]
  • 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

  • 输入: intervals = [[1,4],[4,5]]
  • 输出: [[1,5]]
  • 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
  • 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。

代码如下:

  1. /**
  2. 时间复杂度 : O(NlogN) 排序需要O(NlogN)
  3. 空间复杂度 : O(logN) java 的内置排序是快速排序 需要 O(logN)空间
  4. */
  5. class Solution {
  6. public int[][] merge(int[][] intervals) {
  7. List<int[]> res = new LinkedList<>();
  8. //按照左边界排序
  9. Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));
  10. //initial start 是最小左边界
  11. int start = intervals[0][0];
  12. int rightmostRightBound = intervals[0][1];
  13. for (int i = 1; i < intervals.length; i++) {
  14. //如果左边界大于最大右边界
  15. if (intervals[i][0] > rightmostRightBound) {
  16. //加入区间 并且更新start
  17. res.add(new int[]{start, rightmostRightBound});
  18. start = intervals[i][0];
  19. rightmostRightBound = intervals[i][1];
  20. } else {
  21. //更新最大右边界
  22. rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1]);
  23. }
  24. }
  25. res.add(new int[]{start, rightmostRightBound});
  26. return res.toArray(new int[res.size()][]);
  27. }
  28. }
  1. // 版本2
  2. class Solution {
  3. public int[][] merge(int[][] intervals) {
  4. LinkedList<int[]> res = new LinkedList<>();
  5. Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0], o2[0]));
  6. res.add(intervals[0]);
  7. for (int i = 1; i < intervals.length; i++) {
  8. if (intervals[i][0] <= res.getLast()[1]) {
  9. int start = res.getLast()[0];
  10. int end = Math.max(intervals[i][1], res.getLast()[1]);
  11. res.removeLast();
  12. res.add(new int[]{start, end});
  13. }
  14. else {
  15. res.add(intervals[i]);
  16. }
  17. }
  18. return res.toArray(new int[res.size()][]);
  19. }
  20. }

 

738.单调递增的数字

力扣题目链接(opens new window)

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

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

示例 1:

  • 输入: N = 10
  • 输出: 9

示例 2:

  • 输入: N = 1234
  • 输出: 1234

示例 3:

  • 输入: N = 332
  • 输出: 299

说明: N 是在 [0, 10^9] 范围内的一个整数。

代码如下:

  1. 版本1
  2. class Solution {
  3. public int monotoneIncreasingDigits(int N) {
  4. String[] strings = (N + "").split("");
  5. int start = strings.length;
  6. for (int i = strings.length - 1; i > 0; i--) {
  7. if (Integer.parseInt(strings[i]) < Integer.parseInt(strings[i - 1])) {
  8. strings[i - 1] = (Integer.parseInt(strings[i - 1]) - 1) + "";
  9. start = i;
  10. }
  11. }
  12. for (int i = start; i < strings.length; i++) {
  13. strings[i] = "9";
  14. }
  15. return Integer.parseInt(String.join("",strings));
  16. }
  17. }

java版本1中创建了String数组,多次使用Integer.parseInt了方法,这导致不管是耗时还是空间占用都非常高,用时12ms,下面提供一个版本在char数组上原地修改,用时1ms的版本

  1. 版本2
  2. class Solution {
  3. public int monotoneIncreasingDigits(int n) {
  4. String s = String.valueOf(n);
  5. char[] chars = s.toCharArray();
  6. int start = s.length();
  7. for (int i = s.length() - 2; i >= 0; i--) {
  8. if (chars[i] > chars[i + 1]) {
  9. chars[i]--;
  10. start = i+1;
  11. }
  12. }
  13. for (int i = start; i < s.length(); i++) {
  14. chars[i] = '9';
  15. }
  16. return Integer.parseInt(String.valueOf(chars));
  17. }
  18. }

 

968.监控二叉树

力扣题目链接(opens new window)

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

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

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

示例 1:

  • 输入:[0,0,null,0,0]
  • 输出:1
  • 解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

  • 输入:[0,0,null,0,null,0,null,null,0]
  • 输出:2
  • 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。

提示:

  • 给定树的节点数的范围是 [1, 1000]。
  • 每个节点的值都是 0。

代码如下:

  1. class Solution {
  2. int res=0;
  3. public int minCameraCover(TreeNode root) {
  4. // 对根节点的状态做检验,防止根节点是无覆盖状态 .
  5. if(minCame(root)==0){
  6. res++;
  7. }
  8. return res;
  9. }
  10. /**
  11. 节点的状态值:
  12. 0 表示无覆盖
  13. 1 表示 有摄像头
  14. 2 表示有覆盖
  15. 后序遍历,根据左右节点的情况,来判读 自己的状态
  16. */
  17. public int minCame(TreeNode root){
  18. if(root==null){
  19. // 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
  20. return 2;
  21. }
  22. int left=minCame(root.left);
  23. int right=minCame(root.right);
  24. // 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
  25. if(left==2&&right==2){
  26. //(2,2)
  27. return 0;
  28. }else if(left==0||right==0){
  29. // 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头
  30. // (0,0) (0,1) (0,2) (1,0) (2,0)
  31. // 状态值为 1 摄像头数 ++;
  32. res++;
  33. return 1;
  34. }else{
  35. // 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头,
  36. // 那么本节点就是处于被覆盖状态
  37. return 2;
  38. }
  39. }
  40. }

贪心算法总结篇: 

贪心系列的时候就说了,贪心系列并不打算严格的从简单到困难这么个顺序来讲解。

因为贪心的简单题可能往往过于简单甚至感觉不到贪心,如果我连续几天讲解简单的贪心,估计录友们一定会不耐烦了,会感觉贪心有啥好学的。

但贪心的难题又真的有点难,所以我是简单困难交错着讲的,这样大家就感觉难度适中,而且贪心也没有什么框架和套路,所以对刷题顺序要求没有那么高。

但在贪心系列,我发的题目难度会整体呈现一个阶梯状上升,细心的录友们应该有所体会。

在刚刚讲过的回溯系列中,大家可以发现我是严格按照框架难度顺序循序渐进讲解的,和贪心又不一样,因为回溯法如果题目顺序没选好,刷题效果会非常差!

同样回溯系列也不允许简单困难交替着来,因为前后题目都是有因果关系的,相信跟着刷过回溯系列的录友们都会明白我的良苦用心

每个系列都有每个系列的特点,我都会根据特点有所调整,大家看我每天的推送的题目,都不是随便找一个到就推送的,都是先有整体规划,然后反复斟酌具体题目的结果

那么在贪心总结篇里,我按难易程度以及题目类型大体归个类。

贪心大总结正式开始:

#贪心理论基础

在贪心系列开篇词关于贪心算法,你该了解这些! (opens new window)中,我们就讲解了大家对贪心的普遍疑惑。

  1. 贪心很简单,就是常识?

跟着一起刷题的录友们就会发现,贪心思路往往很巧妙,并不简单。

  1. 贪心有没有固定的套路?

贪心无套路,也没有框架之类的,需要多看多练培养感觉才能想到贪心的思路。

  1. 究竟什么题目是贪心呢?

Carl个人认为:如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟。(并不是权威解读,一家之辞哈)

但我们也不用过于强调什么题目是贪心,什么不是贪心,那就太学术了,毕竟学会解题就行了。

  1. 如何知道局部最优推出全局最优,有数学证明么?

在做贪心题的过程中,如果再来一个数据证明,其实没有必要,手动模拟一下,如果找不出反例,就试试贪心。面试中,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了

就像是 要用一下 1 + 1 = 2,没有必要再证明一下 1 + 1 究竟为什么等于 2。(例子极端了点,但是这个道理)

相信大家读完关于贪心算法,你该了解这些! (opens new window),就对贪心有了一个基本的认识了。

#贪心简单题

以下三道题目就是简单题,大家会发现贪心感觉就是常识。是的,如下三道题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据!

#贪心中等题

贪心中等题,靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。

#贪心解决股票问题

大家都知道股票系列问题是动规的专长,其实用贪心也可以解决,而且还不止就这两道题目,但这两道比较典型,我就拿来单独说一说

#两个维度权衡问题

在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。

在讲解本题的过程中,还强调了编程语言的重要性,模拟插队的时候,使用C++中的list(链表)替代了vector(动态数组),效率会高很多。

所以在贪心算法:根据身高重建队列(续集) (opens new window)详细讲解了,为什么用list(链表)更快!

大家也要掌握自己所用的编程语言,理解其内部实现机制,这样才能写出高效的算法!

#贪心难题

这里的题目如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练!

#贪心解决区间问题

关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。

#其他难题

贪心算法:最大子序和 (opens new window)其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。

贪心算法:加油站 (opens new window)可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。

最后贪心系列压轴题目贪心算法:我要监控二叉树! (opens new window),不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。

#贪心每周总结

周总结里会对每周的题目中大家的疑问、相关难点或者笔误之类的进行复盘和总结。

如果大家发现文章哪里有问题,那么在周总结里或者文章评论区一定进行了修正,保证不会因为我的笔误或者理解问题而误导大家。

所以周总结一定要看!

#总结

在这十八道贪心经典题目中,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚

这也是我认为判断这是一道贪心题目的依据,如果找不出局部最优,那可能就是一道模拟题。

贪心专题汇聚为一张图:

这个图是 代码随想录知识星球 (opens new window)成员:海螺人 (opens new window)所画

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/254212
推荐阅读
相关标签
  

闽ICP备14008679号