zoukankan      html  css  js  c++  java
  • 贪心算法

    贪心算法

    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);
              }
          }
      
  • 相关阅读:
    vue组件重新加载的方法
    事件触发方法获取当前值的写法 (含方法要传2个参数的写法)
    mac 解压 rar压缩文件
    表格
    小米8安装charles证书方法
    视频结构化技术栈全解析
    多目标跟踪全解析,全网最全
    SpringBoot
    技术方案设计的方法
    Java的强引用、软引用、弱引用、虚引用
  • 原文地址:https://www.cnblogs.com/pangqianjin/p/14248578.html
Copyright © 2011-2022 走看看