什么是贪心算法?
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
贪心解决问题的基本思路?
1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每一子问题求解,得到子问题的局部最优解
4.把子问题对应的局部最优解合成原来整个问题的一个近似最优解
例题
1. 分配饼干
题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。
/*
*每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
* */
public int findContentChildren(int[] g, int[] s) {
/*
* 思路:将所有 饼干,孩子的愿望 分别从小到大排序。
* 从小到大遍历每一个孩子的愿望, 按照饼干从小到大的顺序来满足从小到大的孩子的愿望。
* 判断当前块的饼干是否可以满足孩子的愿望。如果不能(那么后面有更大愿望的孩子就更不能满足了),所以刨除此块,继续用下一块来判断;如果能,则累计count。
* 直到饼干遍历结束或者孩子的愿望遍历结束。
* */
Arrays.sort(g);
Arrays.sort(s);
int gi = 0;
int si = 0;
int count = 0;
while (gi < s.length && si < s.length) {
if (s[si] >= g[gi]) {
count++;
gi++;
}
si++;
}
return count;
}
2. 不重叠的区间个数
题目描述:计算让一组区间不重叠所需要移除的区间个数
/*
* 计算让一组区间不重叠所需要移除的区间个数
* */
public int eraseOverlapIntervals(int[][] intervals) {
/*
* 思路:让一组区间不重叠,优先选择区间末尾值小的,这样才能让后面的区间最可能不重叠。
* 换句话说 优先选择区间末尾值小的区间,才能拼接更多的区间,移除更少的区间
* 做法:
* a.首先根据区间末尾值由小到大的顺序 将区间排序
* b.维持一个区间末尾值end
* c.按照排序后的顺序遍历所有区间,如果当前区间不会与上一个区间重叠(判断方式:当前区间的首值是否大于等于end),就更新end
* */
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
int end = Integer.MIN_VALUE;
int removeCount = 0;
for (int i = 0; i < intervals.length; i++) {
if (intervals[i][0] >= end) {
end = intervals[i][1];
} else {
removeCount++;
}
}
return removeCount;
}
相同思路的题目:
a.假设有如下课程,希望尽可能多的将课程安排在一间教室里:
解题思路:选择结束最早的课
b.气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都被刺破。求解最小的投飞镖次数使所有气球都被刺破。也是计算不重叠的区间个数,不过和Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。
3.根据身高和序号重组队列
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
/*
* 根据身高和序号重组队列
* 题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
* */
public int[][] recombination(int[][] people) {
/*
* 解题思路:优先向队伍中插入最高的。原因是,确保了最高的学生的组队条件,后面再向队伍中插入比他矮的学生时,都不会影响他的条件。
* 因为他的条件是:排在他前面的k个学生 身高比他高或者和他一样高。即使比他矮的学生插入到了他的前面也不会影响他,当然插到他后面更不会影响他了
* 做法:
* a.将所有的学生按照身高由大到小的顺序排序,身高相同的情况下,将k小的排在前面。
* b.按照排序后的顺序 不断将学生插入到队列中
* */
Arrays.sort(people, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o2[0] == o1[0] ? o2[1] - o2[1] : o2[0] - o1[0];
}
});
LinkedList<int[]> list = new LinkedList<>();
list.add(people[0]);
for (int i = 1; i < people.length; i++) {
list.add(people[i][1], people[i]);
}
return list.toArray(new int[list.size()][]);
}
4.买卖股票最大的收益
/*
* 题目描述:一次股票交易包含买入和卖出,只进行一次交易,求最大收益。
* */
public int maxProfit(int[] prices) {
int minBuy = Integer.MAX_VALUE;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minBuy) {
minBuy = prices[i];
continue;
}
maxProfit = Math.max(maxProfit, prices[i] - minBuy);
}
return maxProfit;
}
/*
* 题目描述:可以进行多次交易,多次交易之间不能交叉进行,可以进行多次交易。
* 做法:当遇到a[i]-a[i-1]>0时,就把这个差值累计算的利润中
* 思路:
* 1.会不会出现a[i]前面有比a[i-1]还小的情况,不应该累计最多的利润吗?
* 例如:2~5~9。 (5-2)+(9-5)=9-5 这种情况时,已经将5-2的利润累计了,所以不必担心累计不到9-2这种更大利润的情况。宽泛的说:当a<b<c<d时,(b-a)+(c-b)+(d-c)=d-a
* 2. 万一出现2~5~10~9这种情况呢?
* 9之前出现比它更大的元素,这种情况出现时,这段区间的最大利润已经被10截断了,跟9没关系了。 最大利润就是(5-2)+(10-5)=10-2.
* 3.那么9还有可能被累计吗?
* 当2~5~10~9~11时会被累计(5-2)+(10-5)+(11-9)。当2~5~10~9~8时,就直接舍弃9了。
* */
public int maxProfit2(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
5.种植花朵
题目描述:flowerbed 数组中 1 表示已经种下了花朵。花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。
/*
*种植花朵
* 题目描述:flowerbed 数组中 1 表示已经种下了花朵。花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。
* */
public boolean canPlaceFlower(int[] flowerbed, int n) {
/*
* 解题思路:如果当前元素为0,判断当前元素左右是否也为0
* 注意边界值就好
* */
if (flowerbed.length == 1) {
if (flowerbed[0] == 0) n--;
} else {
for (int i = 0; i < flowerbed.length; i++) {
if (flowerbed[i] == 0) {
if (i == 0) {
if (flowerbed[i + 1] == 0) {
n--;
flowerbed[i] = 1;
}
} else if (i == flowerbed.length - 1) {
if (flowerbed[i - 1] == 0) {
n--;
flowerbed[i] = 1;
}
} else if (flowerbed[i - 1] == 0 && flowerbed[i + 1] == 0) {
n--;
flowerbed[i] = 1;
}
}
}
}
if (n <= 0) return true;
return false;
}
6.判断是否为子序列
/*
*判断是否为子序列
* */
public boolean isSubsequence(String s, String t) {
/*做法一*/
char []strT=t.toCharArray();
int index=0;
for(int i=0;i<strT.length;i++){
if(index==s.length()) return true;
if(s.charAt(index)==strT[i]){
index++;
}
}
if(index==s.length()){
return true;
}
return false;
}
public boolean isSubsequence2(String s, String t) {
/*做法二*/
int index=-1;
for(char c:s.toCharArray()){
index=t.indexOf(c,index+1);
if(index==-1) return false;
}
return true;
}
7.修改一个数成为非递减数组
题目描述: 修改一个数,能否成为非递减数组
/*
* 修改一个数成为非递减数组
* 思路:当nums[i]>nums[i+1]时,有两种修改方案,修改nums[i]或nums[i+1]。但要判断修改后,非递减数组这个条件能否成立,成立才可
* 想要修改nums[i],要确保nums[i-1]<=nums[i+1]
* 想要修改nums[i+1],要确保nums[i]<=nums[i+2]
* */
public boolean checkPossibility(int[] nums) {
boolean isModify=false;
for(int i=0;i<nums.length-1;i++){
if(nums[i]>nums[i+1]){
if(isModify) return false;
if(i+1==nums.length-1) return true;
if(nums[i]<=nums[i+2]) {
nums[i+1]=nums[i];
isModify=true;
}else if(i==0||nums[i-1]<=nums[i+1]){
nums[i]=nums[i+1];
isModify=true;
}else {
return false;
}
}
}
return true;
}
8.子数组最大的和
/*
* 子数组最大的和
* 思路:
* a.max作为最大的和,随时更新
* b.用curSum累计,考虑某元素nums[i]时,如果curSum<0,一定会curSum+nums[i]<nums[i]。所以如果curSum<0,就将curSum置为0
* */
public int maxSubArray(int[] nums) {
int max=Integer.MIN_VALUE;
int curSum=0;
for (int i=0;i<nums.length;i++){
curSum+=nums[i];
max=Math.max(max,curSum);
if(curSum<0) curSum=0;
}
return max;
}
9. 分隔字符串使同种字符出现在一起
/*
分隔字符串使同种字符出现在一起
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.
* */
public List<Integer> partitionLabels(String S) {
/*
* 思路:每次遍历到一个元素时,就要考虑后面还有没有相同的元素,如果有,就要分到一组。
* 做法:遍历整个字符串,遍历到元素curElem时,就从后向前查找,找到最后一个与之相同的元素,此时,这两个区间为一组,还要考虑在这个区间的元素
* */
char str[]=S.toCharArray();
char label[]=new char[30];
for(int i=0;i<str.length;i++){
label[str[i]-'a']++;
}
List<Integer> list=new LinkedList<>();
int firstIndex=0;//区间首元素索引
int lastIndex=0;//区间尾元素索引
int curIndex=0;//当前元素
while (firstIndex<str.length){
lastIndex=Math.max(lastIndex,S.lastIndexOf(S.charAt(curIndex)));//每次遍历一个元素,就要找到字符串中最后一个与之相同的元素。并更新区间尾元素索引
if(curIndex==lastIndex){//直到当前元素是区间尾元素(与当前元素相同的最后一个元素是自己,说明同种字符都在一个区间内了),此时首尾元素之间的所有可以分为一组。
list.add(lastIndex-firstIndex+1);
firstIndex=lastIndex+1;//更新下一个区间的首元素索引
curIndex=lastIndex+1;//更新下一个区间的尾元素索引
}else {
curIndex++;
}
}
return list;
}
10.集合覆盖问题:
假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
解题思路:
1.将每个地区可以接收到信号的广播台 的数量 放入到map里。
ID–>2
NV–>3
…
2.遍历每个广播,如果该广播台能覆盖的地区,都可以由其他 广播台覆盖到(map中的value都大于1),那么抛弃该广播台,同时将map中 该广播台能覆盖的地区的value值减1
每次都要选择一个 能覆盖到其他广播台覆盖不到的地区的 广播台。