贪心
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择,而仅根据当前的信息做出选择之后,不管将来有什么结果,这个选择都不能改变。换而言之,贪心法并不是从整体最优考虑,而是在某种意义上的局部最优,这种局部最优并不能保证总能获得全局最优解,但通常能获得较好的近似解。
什么意思呢?举个例子,平时购物找钱的时候,我们一般都先从面值更大的币种开始寻找,而如果要找的零钱不足大面值的金额的时候,我们才会去考虑下一个较小的币种,这其实就是贪心算法。如果银行发行的币种只有面值为 11 , 5 , 1的币种,那么凑齐15元最少的面值数,按照贪心法为一张面值11元和4张面值1元的,一共是5张,但是实际上最优解是3张5元的。看到这里,想必你对贪心有了一定的了解了,下面我们通过力扣题目来寻找贪心的真相。
用最少数量的箭引爆气球,这也是我们没有接触过的区间类型一类的题目,这种题目在力扣上还是比较多的,这种问题的解决方案贪心算法与我们的动态规划有一定的相似之处。实际上在思考的时候,这种区间问题也比正常的动态规划要容易一些,因为它所展现的逻辑性更强,相比之下,更容易得出递推公式,在做这种题目的时候,更重要的是理解题意,下面我们来体会一下。
题目
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend],返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
示例 3:
输入: points = [[1,2],[2,3],[3,4],[4,5]]
输出: 2
思路分析
看完题目,我就觉得解释多余了,题目结构冗杂,先自我化简一下题目的意思,大意为: 若是有区间重叠地方,那么只用一支箭就能够引爆,所以我们要用最少的箭来引爆全部的气球
在这里我们应用一下其他的例子,基本涵盖了所有的情况,我采用了一个类似于示例2的例子. points = [[1,2],[2,3],[2,4],[4,5]]
。我们根据题目的意思,画出了一个y轴射箭,x轴摆放气球的坐标系。四个区间用四种颜色的箭头表示,那么从图解来看结果就一目了然了,两支箭就够了。
图解
看完图解,我觉得大家可能心里都知道该怎么办了,但是代码应该怎么实现呢?很多人都是这样子,思路有了,但是无法付诸于实践,我觉得这种可能就是模板不熟悉或者是基础不够扎实的问题,因为很多类型的题目往往都是如此,可能以前你见过它,但是换了一套衣服你就不认识了,所以还是需要多加练习的,总结出经验。
我们来分析题目在,做贪心做法时,我们先要考虑局部最优的情况,再从局部最优的情况得到整体情况。在这题当中,我们局部最优的情况当然就是一支箭可以引爆多个气球,而全局最优则是用最少的箭引爆全部的气球。那么我们怎么得到局部最优解呢?我们就需要在区间上‘做手脚’了。
这做手脚的第一步,就是要对我们的区间进行排序,有些区间可能给的比较乱,绕的你头昏眼花的。所以第一步先进行排序,那么按照什么规则顺序进行排序呢?右边界亦或者是左边界吗?其实都可以,无非就是遍历顺序换一下,这里我采用了按照左边界的大小进行排序。
排序之后,就是“合并区间”。我为什么打了引号呢?因为这里并不需要真的合并区间。如果要合并的话,按照以下步骤,因为我们的左边界已经排过序了,那么我们只需要比较下一个区间的左边界和上一个区间的右边界 (注意,如果采用右边界和下一个左边界相比较,会造成下标越界) ,我们就能知道两个区间有没有重叠,也就是当points[i][0] <= points[i - 1][1]
的时候,注意的是这里要包括等于,因为边界也作数。那么这个时候,我们就让区间的右边界改变取两个区间的最小值,然后压入一个新的数组来表示处理过的区间,当然这个题目没有必要,但是可能其他的题目需要,顺嘴提一下。我们这里还是需要用到这种方法合并的区间的,当然只需要替换当前遍历的数组的右边界。
而且我们这里并不需要真正的删除掉数组来表示引爆气球,或者开辟一个新的数组保存合并区间的结果,我们定义一个count
变量来表示所需要的最小箭数,如果存在重叠区间,那么我们就不使count
增加,来模拟我们合并了两个数组的过程,而在没有重叠区间的时候,让count
增加,表示还需要另外一支箭.遍历完数组,就得到了结果。
具体代码
class Solution {
private:
static bool compare(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
if (points.size() == 0) return 0;
sort(points.begin(), points.end(), compare);//按照左边界的大小进行排序
int count = 1;//至少需要一支箭
for(int i = 1;i < points.size();i++){
if(points[i][0] > points[i - 1][1]) //下一个的左边界大于上一个右边界,表示没有重叠区间
{
count++; //还需要另外一只箭
}
else{//表示有重叠
points[i][1] = min(points[i][1] , points[i - 1][1]);//更新最小右边界,这个区间内可以引爆重叠区域的气球
}
}
return count;
}
};
代码分析
这里我们需要注意的一个知识点,就是compare函数里面的return a[0] < b[0]
,表示的就是按照第一列的数据的大小来进行排序。并且循环要1开始,否则下标会越界。而当没有区间重叠的时候,就加一支箭,否则存在区间重叠,需要更新区间,以便于后续计算。
总结
其实贪心算法的应用很多,在做问题的时候,往往意识不到是否需要用贪心算法。它与动态规划十分相似,我觉得贪心算法算是一种特殊意义的动态规划,它能做的,动态规划可以做,它不能的,动态规划还是能做,只是在某些问题上面,贪心算法比动态规划更优化,也更简单。所以说,还是得练,还是得总结,没办法,就这样!哈哈哈
我是小白,我们一起学习力扣!