1.贪心算法的特征
贪心算法是求解一个问题全局最优解的算法,但是在求解全局最优解的时候可以分成多个局部最优解的加和。贪心主要满足如下三个特征:
1.最优子结构
即一个问题的最优解包含其子问题的最优解的时候,就可以称之为最优子结构。这个概念和动态规划是一个概念,假设问题的解为f(n),最优子结构就是可以得到f(n)和f(n-1)之间的一个递推公式。
2.最优贪心选择属性
最优贪心选择属性其实就是无后效性,即全局最后解可以分成多个局部最优解的加和。这是贪心算法和动态规划的本质所在。
2.贪心算法的证明
在解决最优化算法问题中,一般考虑就3种算法,贪心,动态规划和回溯,如果问题满足最优子结构,就可以考虑贪心算法,但是在此之前,我们需要证明题目是否满足最优贪心属性这个特征,这也是贪心算法的难点所在。
一般证明方法,有如下几种:
1.反证法
前面说过贪心算法其实就是多个局部最优解的加和,我们可以通过经验得到这个需要解决的局部最优问题,并且通过加和,得到理想全局最优解,比如为s=a1+a2+....an,然后通过替换某个局部最优解比如am替换为ax,然后在比较两个解谁是最优,得到答案。
2.数学归纳法
同高中数学。
而实际过程中,其实往往是通过经验+反证进行判断。接下来我会证明一些题目为什么可以采用贪心算法。
3.贪心算法模板
4.常见贪心算法分类
1.硬币找零问题。特征:大面值硬币能由多个小面值硬币组成。
5.贪心算法的举例
5.1 零钱找零问题
1.题目描述
假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?
2.证明
这道题是最经典的贪心算法的应用问题。但是为什么可以采用贪心算法呢,我们证明一下。
首先我们凭借生活中的常识,可以知道,应该是先补面值大的,大的超出剩余额度后,再补面值小的币。假设按照这种找钱方式,1元、2元、5元、10元、20元、50元、100元分别用a,b,c,d,e,f,g这么多张,所以K=a+2+5c+10d+20e+50f+100g,然后我们随机替换其中一张币,假设50元的币被替换出来,50可以看出2张20+1张10元,所以替换过后比以前会多两张币,采用贪心算法是合理的。
可以看出这道题能使用贪心算法的本质原因是,每张大面值钞票的面额都能由小面值钞票面额构成。
3.代码
public class Greedy {
private int arr[] = new int[]{10,5,2,1};
int num = 0;
int i = 0;
int solveMoney(int target) {
while (target > 0) {
if(target >= arr[i]) {
target = target - arr[i];
num ++;
}else {
i ++;
}
}
return num;
}
}
4.备注
上面这道问题在leetcode 322题由类似场景
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
这道题,也是最优化问题,但是不满足上面说的大面值硬币一定可以替换成多个小面值硬币这个条件。所以采用动态规划求解会更加方便。
5.2 分发饼干问题
1.题目描述
2.证明
我们凭借经验可以知道应该从胃口小的孩子开始分发。现在假设按照这种方式能够分发给m个学生,分别是g[1]到g[m],现在假设有一个学生g[x]的蛋糕s[x]被分配给了比g[x]大的学生g[y],所以g[x]只能去向周围s[x-1]和s[x+1]探索能否被分派给他,可能s[x-1]和s[x+1]已经被分派给其他学生或者不满足条件。所以这个时候g[x]不能被分派蛋糕,比假设常见少一位。这道题其实是区间问题。
3.代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
//1.将孩子的需求和饼干从小到大排序
Arrays.sort(g);
Arrays.sort(s);
int i = 0;
int j = 0;
int num = 0;
while (i < g.length && j < s.length) {
if(s[j] >= g[i]) {
num ++;
j ++;
i ++;
}else {
j ++;
}
}
return num;
}
}
5.3 跳跃游戏
1.题目描述
2.分析
1.如果第一个元素为3便表示,便表示后面3个格子都是可以起跳的;
2.每次进入循环的时候应该先判断该格是否能够到达,即判断当maxR是否能够到达i,从0往后遍历元素,每次更新当前能够跳到最远的格子,假设当前为第n个格子,便从该格子能够跳到maxR = Math.max(n+num[n],maxR)这么远;
3.如果maxR>= nums.length - 1,便跳出循环
3.代码
class Solution {
public boolean canJump(int[] nums) {
int maxR = 0;
for (int i = 0; i < nums.length; i++) {
if (maxR < i) {
return false;
}
maxR = Math.max(maxR, i + nums[i]);
if (maxR >= nums.length - 1) {
return true;
}
}
return false;
}
}
5.4 无重叠区间
1.题目描述
2.分析
对于区间问题,我们可以直接根据区间左端点和右端点进行排序,然后求出每次子空间的一个最优解。
分析可得,在堆区间进行左边界排序过后,依次进行区间组合,假设现在黑色区间在取出部分区间后都是没有交集的区间,然后我们对下一步红色区间进行处理,这里有3种情况。
1.红色区间与黑色区间没有交集,不用去除区间,向后遍历。
2.红色区间与黑色区间有交集,并且右端点大于黑色区间的右端点,应该保证前面占用空间越小与后面区间交集的概率越小,所以应该去除红色区间,即count++,且向后遍历。
3.红色区间与黑色区间有交集,并且右端点小于于黑色区间的右端点,这时应该去除黑色区间。
针对上述的两种情况,每次最大的右端点应该更新为min(当前右端点,红色区间右端点)。
3.代码
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length <= 1) {
return 0;
}
// 先根据左侧排序
Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0]));
int count = 0;
for (int i = 1; i < intervals.length; i++) {
// 如果第i个左区间小于第i-1个右区间,便证明有交集
if (intervals[i][0] < intervals[i - 1][1]) {
// 移除右侧元素
count++;
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
}
}
return count;
}
}
4.备注
这道题按照右区间排序也是可以的。
______________to be continue___________________