贪心算法
2.1 算法解释
顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。
举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。
2.2 分配问题
-
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; }
-
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 区间问题
- 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 练习
基础难度
-
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; }
- 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; } }
-
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; }
-
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; }
-
进阶难度
-
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); } }