五大常用算法:
1、递归与分治:直接或者间接不断反复调用自身来达到解决问题的方法。这就要求原始问题可以分解成相同问题的子问题。主要解决的是阶乘、斐波纳契数列、汉诺塔类似问题。
2、动态规划:基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
与分治不同在于:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
3、贪心算法:理解的话就是从字面意思理解“贪”,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
4、回溯法:回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。通常与“剪枝”放在一起使用。
5、分支限界法:和回溯法相似,也是一种搜索算法,但回溯法是找出问题的许多解,而分支限界法是找出原问题的一个解。或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
上面五种常用算法每种算法其实都有自己解题领域和思想,同样也拥有自己的解题方式和模板提供参考。本章只针对贪心算法进行总结和归纳,其他几种陆陆续续也会整理出来🤗。
五常算法之三之贪心
先举个栗子🌰
举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹 果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的 贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全 局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的 策略。
解题实现框架:
从问题的某一初始解出发;
while (能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;
贪心算法的基本思路:
- 建立数学模型来描述问题
- 把求解的问题分成若干个子问题
- 对每个子问题求解,得到子问题的局部最优解
- 把子问题的解局部最优解合成原来问题的一个解
贪心算法存在的问题:
- 不能保证求得的最后解是最佳的
- 不能用来求最大值或最小值的问题
- 只能求满足某些约束条件的可行解的范围
LeetCode刷题
LeetCode上没有做相应的题型汇总,只能散列题号去刷,主要针对分配问题和区间问题典型题型
分配问题
第一题快速导航:
题目描述:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;
并且每块饼干 j,都有一个尺寸 s[j] 。
如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。
你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
题解:
因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。
为了尽量使得剩下的饼干可 以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这 个孩子。
满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到 没有满足条件的饼干存在。
简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。
至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。 这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。
注意: 对数组或字符串排序是常见的操作,方便之后的大小比较。
注意:在之后的讲解中,若我们谈论的是对连续空间的变量进行操作,我们并不会明确区分数组和字符串,因为他们本质上都是在连续空间上的有序变量集合。一个字符串“abc”可以被看作一 个数组 [‘a’,‘b’,‘c’]。
代码:
套用模板
从问题的某一初始解出发;
while (能朝给定总目标前进一步) { 利用可行的决策,求出可行解的一个解元素; } 由所有解元素组合成问题的一个可行解;
class Solution {
public int findContentChildren(int[] g, int[] s) {
/**
* 排序的目的是贪心
* 为了找到饭量最小的那个孩子先喂饱
* 同时找到饼干大小排序方便拿取实现最恰当的饼干喂给最恰当的孩子的最佳实现
*/
Arrays.sort(g);
Arrays.sort(s);
int child = 0;
int cook = 0;
while(child < g.length && cook <s.length){
if(g[child] <= s[cook]){
//可行解 吃饱一个继续喂下个
++child;
}
//往下遍历饼干
++cook;
}
return child;
}
}
第二题快速导航:
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
示例 2:
输入: [1,2,2] 输出: 4 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这已满足上述两个条件。
题解:
做完了题目 455,你会不会认为存在比较关系的贪心策略一定需要排序或是选择?
虽然这一 道题也是运用贪心策略,但我们只需要简单的两次遍历即可:
1、把所有孩子的糖果数初始化为 1;
2、先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的 糖果数加 1;
3、再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数 不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
通过这两次遍历, 分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一 侧的大小关系。
在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历 更新后的结果为 [2,1,2]。
代码:
套用模板
从问题的某一初始解出发;
while (能朝给定总目标前进一步) { 利用可行的决策,求出可行解的一个解元素; } 由所有解元素组合成问题的一个可行解;
class Solution {
public int candy(int[] ratings) {
int size = ratings.length;
if(size < 2){
return size;
}
int[] array = new int[size];
//初始化数组内数字为1
for(int i = 0;i<size;i++){
array[i] = 1;
}
//正向对比 左比右
for(int n = 1;n<size;n++){
if(ratings[n-1] < ratings[n]){
//右子大 则右子比左子加一
array[n] = array[n-1] + 1;
}
}
//反向对比 右比左
for(int m = size -1; m>0;m--){
if(ratings[m-1] > ratings[m]){
//左子大 因为有上面一层循环保证比较 右子比左子大则右子比左子多
//现在需要保证 左子大则左子要比右子多 至于取数则在自己已有数据上和右子加一作对比取大值
array[m-1] = Math.max(array[m-1],array[m]+1);
}
}
int sum = 0;
for(int q = 0;q<size;q++){
sum = sum + array[q];
}
return sum;
}
}
本文主要是针对贪心算法中的分配问题做了解析总结(点比较),贪心算法还有一种题型是区间问题(段比较),区间问题总结讲再下一博文总结归纳,Thanks♪(・ω・)ノ。