算法-贪心

本文详细介绍了贪心算法的概念、策略和在解决分发饼干、摆动序列、最大子序和、跳跃游戏等一系列IT问题中的应用,展示了如何通过局部最优决策推导全局最优解。
摘要由CSDN通过智能技术生成

一、什么是贪心:

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

二、贪心的策略:

举反例,如果想不到反例,那么就试一试贪心

三、步骤:

  1. 将问题分解为若干个子问题;
  2. 找到适合的贪心策略;
  3. 求子问题的最优解;
  4. 局部最优推全局最优

四、例题:

  1. 分发饼干

    1. 题目:

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

      对每个孩子 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. 思路
      • 用尽可能的小饼干喂胃口小的孩子
    3. 题解:
      class Solution {
          public int findContentChildren(int[] g, int[] s) {
              if (s.length == 0) {
                  return 0;
              }
              Arrays.sort(g);
              Arrays.sort(s);
              int res = 0;
              //小饼干先喂小胃口
              for (int i = 0; i < s.length && res < g.length; i++) {
                  if (s[i] >= g[res]) {
                      res++;
                  }
                  continue;
              }
              return res;   
          }
      }
    4. 总结:思考局部最优,如何推出全局最优,以及是否存在反例
  2. 摆动序列

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

      2. 示例 1:

        输入:nums = [1,7,4,9,2,5]
        输出:6
        解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
    2. 思路
      1. 包含所有的波峰与波谷
    3. 题解
      class Solution {
          public int wiggleMaxLength(int[] nums) {
              if (nums.length < 2) {
                  return nums.length;
              }
              int res = 1;
              int pre = 0;
              for (int i = 1; i < nums.length; i++) {
                  int cur = nums[i] - nums[i - 1];
                  if (cur > 0 && pre <= 0 || cur < 0 && pre >= 0) {
                      res++;
                      pre = cur;
                  }
              }
              return res;
          }
      }
    4. 总结:整个序列有最多的峰值
  3. 最大子序和

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

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

    2. 思路
      1. 局部最优:当前“连续和”为负数时放弃,从下一个元素重新计算“连续和”。负数加上下一个元素 “连续和”只会越来越小。

      2. 全局最优:选取最大“连续和”

    3. 题解
      class Solution {
          public int maxSubArray(int[] nums) {
              int res =  Integer.MIN_VALUE;
              int sum = 0;
              for (int i = 0; i < nums.length; i++) {
                  sum += nums[i];
                  res = Math.max(res, sum);
                  if (sum < 0) {
                      sum = 0;
                  }
              }
              return res;
          }   
      }
    4. 总结:
  4. 跳跃游戏

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

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

    2. 思路
      1. 扩大最大范围,判断最大范围能否覆盖数组长度;
    3. 题解
      class Solution {
          public boolean canJump(int[] nums) {
              if (nums.length < 2) {
                  return true;
              }
              int range = 0;
              for (int i = 0; i <= range; i++) {
                  if (range >= nums.length - 1) {
                      return true;
                  }
                  range = Math.max(range, nums[i] + i);
              }
              return false;
          }
      }
    4. 总结:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
  5. 跳跃游戏II

    1. 题目
      1. 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

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

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

    2. 思路
      1. 每一次都跳到最大范围;
      2. 最大范围变化几次,则需要几步;
    3. 题解
      class Solution {
          public int jump(int[] nums) {
              if (nums.length == 1) {
                  return 0;
              }
              int res = 0;
              int range = 0;
              while (range < nums.length - 1) {
                  int temp = 0;
                  for (int i = 0; i <= range; i++) {
                      temp = Math.max(temp, i + nums[i]);
                  }
                  range = temp;
                  res++;
              }
              return res;
          }
      
          //优化
          public int jump(int[] nums) {
              int range = 0;
              int res = 0;
              int end = 0;
              for (int i = 0; range < nums.length - 1; i++) {
                  end = Math.max(end, i + nums[i]);
                  if (i == range) {
                      res++;
                      range = end;
                  }
              }
              return res;
          }
      }
    4. 总结:
  6. K次取反最大化的数组和

    1. 题目
      1. 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k 次。可以多次选择同一个下标 i 。以这种方式修改数组后,返回数组 可能的最大和 。

    2. 思路
      1. 先从最小的负数开始操作,直至k = 0 || 没有负数
      2. 然后操作一次或者0次最小的整数;
      3. 累加数组和
    3. 题解
      class Solution {
          public int largestSumAfterKNegations(int[] nums, int k) {
              Arrays.sort(nums);
              for (int i = 0; i < nums.length && k > 0 && nums[i] < 0; i++, k--) {
                  nums[i] = 0 - nums[i];
              }
              Arrays.sort(nums);
              nums[0] = k % 2 == 0 ? nums[0] : 0 - nums[0];
              return Arrays.stream(nums).sum();
          }
      }
    4. 总结:存在负数:让绝对值大的负数变为正数;无负数:只找数值最小的正整数进行反转
  7. 加油站

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

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

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

    2. 思路
      1. 遍历cur += gas[i] - cost[i];
      2. 一旦cur< 0,则之前遍历的都不是起点;
      3. 整个消耗小于0,则不存在符合条件的起点;
    3. 题解
      class Solution {
          public int canCompleteCircuit(int[] gas, int[] cost) {
              int start = 0;
              int cur = gas[0] - cost[0];
              int sums = gas[0] - cost[0];
              for (int i = 1; i < gas.length; i++) {
                  sums += gas[i] - cost[i];
                  if (cur < 0) {
                      start = i;
                      cur = 0;
                  }
                  cur += gas[i] - cost[i];
              }
              return sums < 0 ? -1 : start;
          }
      }
    4. 总结:
  8. 分发糖果

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

        你需要按照以下要求,给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。

      2. 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
    2. 思路:
      1. 存在左右两个维度;
      2. 逐一维度进行比较;
      3. 去最大值,则满足要求;
    3. 题解
      class Solution {
          public int candy(int[] ratings) {
              int res = 0;
              //比左孩子
              int[] left = new int[ratings.length];
              //比右孩子
              int[] right = new int[ratings.length];
              for (int i = 0, j = ratings.length - 1; i < ratings.length && j > -1; i++, j--) {
                  if (i > 0 && ratings[i] > ratings[i - 1]) {
                      left[i] = left[i - 1] + 1;
                  } else {
                      left[i] = 1;
                  }
      
                  if (j < ratings.length - 1 && ratings[j] > ratings[j + 1]) {
                      right[j] = right[j + 1] + 1;
                  } else{
                      right[j] = 1;
                  }
              }
              for (int i = 0; i < ratings.length; i++) {
                  res += Math.max(left[i], right[i]);
              }
              return res;
          }
      }
    4. 总结:
  9. 根据身高重建队列

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

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

    2. 思路
      1. 根据身高降序,身高相同,位置小的在前
      2. 根据位置插入
    3. 题解
      class Solution {
          public int[][] reconstructQueue(int[][] people) {
              // 根据身高降序,身高相同,位置小的在前
              Arrays.sort(people, new Comparator<int[]>() {
                  @Override
                  public int compare(int[] o1, int[] o2) {
                      if (o1[0] == o2[0]) {
                          return o1[1] - o2[1];
                      }
                      return o2[0] - o1[0];
                  }
              });
              List<int[]> list = new ArrayList<>();
              for (int[] per : people) {
                  list.add(per[1], per);
              }
              return list.toArray(new int[list.size()][2]);
          }
      }
    4. 总结:两个维度一起考虑一定会顾此失彼,先确定一个维度
  10. 最少数量引爆气球

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

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

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

    2. 思路
      1. 根据start从小到大排序
      2. 遍历不断扩大区间,重合区间则箭数++;
      3. 不重合,则重置区间
    3. 题解
      class Solution {
          public int findMinArrowShots(int[][] points) {
              if (points.length < 2) {
                  return points.length;
              }
              Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
              int range = points[0][1];
              int res = 1;
              for (int i = 1; i < points.length; i++) {
                  if (points[i][0] <= range) {
                      range = Math.min(range, points[i][1]);
                      continue;
                  } 
                  range = points[i][1];
                  res++;
              }
              return res;
          }
      }
    4. 总结
  11. 无重叠区间

    1. 题目
      1. 给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 
    2. 思路
      1. 对start进行升序排序
      2. 遍历
      3. 若重叠res++,去除end大的区间,达到局部最优
      4. 推出全局最优
    3. 题解
      class Solution {
          public int eraseOverlapIntervals(int[][] intervals) {
              Arrays.sort(intervals, new Comparator<int[]>(){
                  @Override
                  public int compare(int[] o1, int[] o2){
                      return o1[0] - o2[0];
                  }
              });
              //Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
              int res = 0;
              for (int i = 1; i < intervals.length; i++) {
                  if (intervals[i][0] < intervals[i - 1][1]) {
                      intervals[i][1] = Math.min(intervals[i][1], intervals[i - 1][1]);
                      res++;
                  }
              }
              return res;
          }
      }
    4. 总结:先确定一个维度,避免顾此失彼
  12. 划分字母区间

    1. 题目
      1. 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。返回一个表示每个字符串片段的长度的列表。

    2. 思路
      1. 遍历字符串,记录每个字母的起始位置与终止位置
      2. 起始位置升序排序
      3. 合并区间
    3. 题解
      class Solution {
          public List<Integer> partitionLabels(String s) {
              int[][] arr = new int[26][2];
               for (int[] array : arr) {
                   Arrays.fill(array, -1);
              }
              for (int i = 0; i < s.length(); i++) {
                  int index = s.charAt(i) - 'a';
                  if (arr[index][0] == -1) {
                      arr[index][0] = i;
                  }
                  arr[index][1] = i;
              }
              Arrays.sort(arr, (a, b) -> Integer.compare(a[0], b[0]));
              List<Integer> list = new ArrayList<>();
              int start = 0;
              int end = 0;
              for (int i = 0; i < arr.length; i++) {
                  if (arr[i][0] == -1) {
                      continue;
                  }
                  if (arr[i][0] <= end) {
                      end = Math.max(arr[i][1], end);
                  } else {
                      list.add(end - start + 1);
                      start = arr[i][0];
                      end = arr[i][1];
                  }
              }
              list.add(end - start + 1);
              return list;
          }
      }
    4. 总结:
  13. 合并区间

    1. 题目
      1. 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
    2. 思路
      1. 根据start升序
      2. 重叠,则合并区间
      3. 不重叠,则记录;
    3. 题解
      class Solution {
          public int[][] merge(int[][] intervals) {
              Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
              List<int[]> list = new ArrayList<>();
              int start = intervals[0][0];
              int end = intervals[0][1];
              for (int i = 0; i < intervals.length; i++) {
                  if (intervals[i][0] <= end) {
                      end = Math.max(intervals[i][1], end);
                  } else {
                      list.add(new int[]{start, end});
                      start = intervals[i][0];
                      end = intervals[i][1];
                  }
              }
              list.add(new int[]{start, end});
              return list.toArray(new int[list.size()][]);
          }
      }
    4. 总结:
  14. 单调递增的数字

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

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

    2. 思路
      1. 从个位往前遍历
      2. 若小于前一位,则这位及以后均变为9,前一位减1
    3. 题解
      class Solution {
          public int monotoneIncreasingDigits(int n) {
              char[] ch = String.valueOf(n).toCharArray();
              int minus = 0;
              for (int i = ch.length - 1; i >= 0; i--) {
                  if (i > 0 && ch[i] - minus < ch[i - 1]) {
                      for (int j = i; j < ch.length; j++) {
                          ch[j] = '9';
                      }
                      minus = 1;
                      continue;
                  }
                  ch[i] -= minus;
                  minus = 0;
              }
              int res = 0;
              for (int i = 0; i < ch.length; i++) {
                  res = (ch[i] - '0') + res * 10;
              }
              return res;
          }
      }
    4. 总结:
  15. 监控二叉树

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

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

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

    2. 思路
      1. 为覆盖所有节点,从叶子节点往根节点遍历(递归)
      2. 确定三种状态:-1监控无覆盖,0监控覆盖,1监控节点
    3. 题解
      class Solution {
          int res = 0;
          public int minCameraCover(TreeNode root) {
              //-1监控无覆盖,0监控覆盖,1监控节点
              //叶子结点父节点安置监控
              return dfs(root) == -1 ? ++res : res;
          }
          
          public int dfs(TreeNode root) {
              if (root == null) {
                  return 0;
              }
              int left = dfs(root.left);
              int right = dfs(root.right);
              if (left == -1 || right == -1) {
                  res++;
                  return 1;
              }
              if (left == 0 && right == 0) {
                  return -1;
              }
              return 0;
          }
      }
  16. 总结:局部最优-让叶子节点的父节点安摄像头,所用摄像头最少。整体最优0全部摄像头数量所用最少!

    未完待续......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值