贪心算法

贪心算法

2.1 算法解释

​ 顾名思义,贪心算法贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。

​ 举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。

2.2 分配问题

  1. Assign Cookies(Easy)

    题目描述

    ​ 有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃最多一个饼干,且只有饼干的大小大于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。

    输入输出样例

    ​ 输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量。

    input: [1, 2], [1, 2, 3]
    
    output: 2
    

    ​ 在这个样例中,我们可以给两个孩子喂[1,2]、[1,3]、[2,3]这三种组合中的任意一种。

    题解

    ​ 因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这个孩子。满足了这个孩子之后,我们采取同样的策略,也考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。

    ​ 简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。

    ​ 至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个孩子可以满足条件。

    注意 对数组或字符串排序是常见的操作,方便之后的大小比较。

    private static int findContentChildren(List<Integer> children, List<Integer> cookies){
            children.sort(Comparator.naturalOrder());
            cookies.sort(Comparator.naturalOrder());
            int child=0, cookie=0;
            while(child<children.size() && cookie<cookies.size()){
                if(children.get(child) <= cookies.get(cookie)){
                    child++;
                }
                cookie++;
            }
            return child;
        }
    
    1. Candy(Hard)

      题目描述

      ​ 一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比身旁的孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少糖果。

      输入输出样例

      ​ 输入是一个数组,表示孩子的评分。输出是最少糖果的数量。

      input: [1, 0, 2]
      output: 5
      

      题解

      ​ 把所有孩子的糖果初始化为1;先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加1.通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略为,在每次遍历中,只考虑并更新相邻一侧的大小关系。

      ​ 在样例中,我们初始化糖果的分配为[1, 1, 1],第一次遍历更新后的结果为[1, 1, 2],第二次遍历更新后的结果为[2, 1, 2]。

      private static int candy(List<Integer> ratings){
              int size = ratings.size();
              if(size<2){
                  return size;
              }
              List<Integer> num = new ArrayList<>();
              for(int i=0; i<size; i++) num.add(1);
              for(int i=1; i<size; i++){
                  if(ratings.get(i) > ratings.get(i-1)){
                      num.set(i, num.get(i-1)+1);
                  }
              }
              for(int i=size-1; i>0; i--){
                  if(ratings.get(i)<ratings.get(i-1) && num.get(i-1)<=num.get(i)){
                      num.set(i-1, num.get(i)+1);
                  }
              }
              return num.stream().mapToInt(Integer::intValue).sum();
          }
      

2.3 区间问题

  1. Non-overlapping Intervals(Medium)

题目描述

​ 给定多个区间,计算这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。

输入输出样例

​ 输入是一个数组,数组由多个长度固定为2的数组组成,表示区间的开始和结尾。输出一个整数,表示需要移除的区间数量。

input: [[1, 2], [2, 4], [1, 3]]
output: 1

​ 在这个样例中,我们可以移除区间[1, 3],使得剩余的区间[[1, 2], [2, 4]]互不重叠。

题解

​ 在选择要保留的区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为:优先保留结尾小且互不相交的区间。
​ 具体实现方法为:先把区间按照结尾的大小进行升序排列,每次选择结尾最小且和前一个选择区间互不重叠的区间。
​ 在样例中,排序后的数组为[[1, 2], [1, 3], [2, 4]]。按照我们的贪心策略,首先初始化为区间[1, 2];由于[1, 3]和[1, 2]相交,我们跳过该区间;由于[2, 4]与[1, 2]不相交,我们将其保留。因此最终保留的区间为[[1, 2], [2, 4]]。
注意 需要根据实际情况判断按区间开头排序还是区间结尾排序。

private static int eraseOverlapIntervals(List<ArrayList<Integer>> intervals){
        if(intervals.isEmpty()){
            return 0;
        }
        int n=intervals.size();
        Collections.sort(intervals, new Compare());
        int total = 0, prev = intervals.get(0).get(1);
        for(int i=1; i<n; i++){
            if(intervals.get(i).get(0) < prev){
                total++;
            }else{
                prev = intervals.get(i).get(1);
            }
        }
        return total;
    }

    private static class Compare implements Comparator<ArrayList<Integer>>{

        @Override
        public int compare(ArrayList<Integer> o1, ArrayList<Integer> o2) {
            if(o1.get(1).equals(o2.get(1))) return 0;
            return o1.get(1) > o2.get(1)? 1: -1;
        }
    }

2.4 练习

基础难度

  1. Can Place Flowers(Easy)

    采取什么样的贪心策略,可以种植最多的花朵呢?

    题目描述

    ​ 有一个长长的花坛,其中某些地块种植了一些花,有些没有。不能在相邻的地块上种花。

    ​ 给定一个包含整数0和1的花坛和一个整数n,其中0表示可以种花,1表示已经种了花。

    ​ 如果可以在不违反相邻地块不能种花的规则下种植n个花,可以则返回true,否则false。flowerbed.size() >=1, n>=0

    输入输出样例

    样例1:

    input: flowerbed = [1, 0, 0, 0, 1], n = 1
    output: true
    

    样例2:

    input: flowerbed = [1, 0, 0, 0, 1], n = 2
    output: false
    

    题解

    贪心策略:只有连续的三个0(即000)的中间位置可以种花。

    private static boolean canPlaceFlowers(List<Integer> bed, int n){
            int flowers=0, head=0, tail=bed.size()-1;
            if(bed.size()==1 && bed.get(0)==0){
                return true;
            }
            for(int i=head; i<=tail; i++){
                // 首
                if(i==head && bed.get(i)==0 && bed.get(i+1)==0) {
                    bed.set(1, 0);
                    flowers++;
                // 尾
                } else if(i==tail && bed.get(i)==0 && bed.get(tail-1)==0){
                    bed.set(tail, 1);
                    flowers++;
                // 中间
                } else if(i!=head&&i!=tail && bed.get(i-1)==0 && bed.get(i+1)==0){
                    bed.set(i, 1);
                    flowers++;
                }
            }
            return flowers>=n;
        }
    
    1. Minimum Number of Arrows to Burst Balloons(Medium)

    这道题和题目435非常类似,但是稍有不同。

    题目描述

    ​ 有一些气球散布在二维空间中。对于每个气球,给出它的水平方向的开始和结束坐标。由于它是水平的,因此y坐标无关紧要,只需要水平方向的起点和终点坐标。

    ​ 可以沿x轴垂直地射出箭头,如果xstart<=x<=send,则带有xstart和xend的气球会被x处射出的箭头射到而爆炸。射出的箭头会无限向上飞行。

    ​ 给定一个数组,其中points[i] = [xstart, xend],求解爆破所有气球需要的最小箭头数。

    样例输入输出

    input: points=[[10, 16], [2, 8], [1, 6], [7, 12]]
    output: 2
    Explanation: One way is to shoot one arrow for example at x = 6 (bursting the balloons [2,8] and [1,6]) and another arrow at x = 11 (bursting the other two balloons).
    

    题解

    贪心策略:尽量使气球重叠,按xend升序排列,一开始需要一只箭(射在x=points[0]的xend处);看这支箭能够穿透最右侧的一只气球points[i],这只气球的右侧下一个气球points[i+1]无法被穿透,需要额外的一只箭来穿透,这只额外的箭射在x=points[i]的xend处。

    private static int findMinArrowShots(List<ArrayList<Integer>> points){
            int arrows=1, size=points.size();
            if(size<2) return size;
            points.sort(new Compare());
            for(int i=0; i<size; i++){
                for(int j=i+1; j<size; j++){
                    if(points.get(i).get(1) <= points.get(j).get(0)){
                        arrows++;
                        break;
                    }
                }
            }
            return arrows;
        }
    
        // point = [xstart, xend], 按 xend升序排列
        private static class Compare implements Comparator<ArrayList<Integer>> {
    
            @Override
            public int compare(ArrayList<Integer> o1, ArrayList<Integer> o2) {
                if(o1.get(1).equals(o2.get(1))) return 0;
                return o1.get(1) > o2.get(1)? 1: -1;
            }
        }
    
    1. Partition Labels(Medium)

      为了满足此贪心策略,需要一些预处理。

      给出小写的英文字母字符串S,希望将此字符串划分为尽可能多的部分,以便每个字母只能出现在其中的一个部分,并返回代表这些部分大小的整数列表。

      样例输入输出

      input: S = "ababcbacadefegdehijhklij"
      Output: [9,7,8]
      
      Explanation:
      The partition is "ababcbaca", "defegde", "hijhklij".
      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.
      

      题解

      start表示某一部分的开始索引,end表示这一部分的结束索引。

      每次for循环在这个部分中,根据每个字母的最后出现位置,决定是否更新end的值。下一部分的start=上一部分的end+1,end=这一部分中最后出现字母的索引。

      private static List<Integer> partitionLabel(String s){
          HashMap<Character, Integer> map = new HashMap<>();
          for(int i=0; i<s.length(); i++){
              map.put(s.charAt(i), s.lastIndexOf(s.charAt(i)));
          }
      
          int start = 0, end;
          List<Integer> results = new ArrayList<>();
          while (start<s.length()){
              end = map.get(s.charAt(start));
              for(int j=start; j<end; j++){
                  char c = s.charAt(j);
                  end = Math.max(map.get(c), end);
              }
              results.add(end - start + 1);
              start = end + 1;
          }
          return results;
      }
      
      1. Best Time to buy and Sell Stock II (Easy)

        假设有一个数组,里面第i个元素尾第i天股票的价格。

        设法找到最大的利润,必须全部卖出才能买下一次。不限制交易次数。

        样例输入输出

        Input: [7,1,5,3,6,4]
        Output: 7
        Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
                     Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
        
        Input: [1,2,3,4,5]
        Output: 4
        Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
                     Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
                     engaging multiple transactions at the same time. You must sell before buying again.
        
        Input: [7,6,4,3,1]
        Output: 0
        Explanation: In this case, no transaction is done, i.e. max profit = 0.
        

        题解

        这是一个最大连续增长,再求和的问题。

        贪心策略:明天的价格高于今天,今天就买,明天卖。

        private static int maxProfit(List<Integer> prices){
                int profit=0;
                for(int i=0; i<prices.size()-1; i++){
                    int today = prices.get(i);
                    int tomorrow = prices.get(i+1);
                    if(today<tomorrow){
                        profit += tomorrow - today;
                    }
                }
                return profit;
            }
        

进阶难度

  1. Queue Reconstruction By Height(Medium)

    需要同时插入和排序操作。

    给定一个people[[height, n], [height, n], ...]数组,height表示这个人的身高,n表示前面有n个人的身高大于等于他自身的身高。

    求解返回的排好序的数组。

    样例输入输出

    Input: people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
    Output: [[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
    

    题解

    首先按照身高降序排列,如果身高相同就按n升序排列。

    然后遍历数组,按n的值插入对应位置。

    ArrayList中这个插入操作容易报下标越界异常,所以我先add再remove。

    private static List<ArrayList<Integer>> reconstructQueue(List<ArrayList<Integer>> people){
            people.sort(new Compare());
            int size = people.size();
            for(int i=0; i<size; i++){
                ArrayList<Integer> person = people.get(i);
                int currentIndex = people.indexOf(person);
                int targetIndex = people.get(i).get(1);
                people.add(targetIndex, person);
                if(targetIndex<currentIndex){
                    people.remove(people.lastIndexOf(person));
                }else{
                    people.remove(person);
                }
            }
            return people;
        }
    
        // person[height, n], 默认按 height降序,相同则按 n升序
        private static class Compare implements Comparator<ArrayList<Integer>> {
    
            @Override
            public int compare(ArrayList<Integer> o1, ArrayList<Integer> o2) {
                if(o1.get(0).equals(o2.get(0))) return o1.get(1) - o2.get(1);
                return o2.get(0) - o1.get(0);
            }
        }
    
posted @ 2021-01-07 20:32  pangqianjin  阅读(156)  评论(0编辑  收藏  举报