文章目录
2 最易懂的贪心算法
2.1 算法解释
- 顾名思义,贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。举一个最简单的例子:小明和小王喜欢吃苹果,小明可以吃五个,小王可以吃三个。已知苹果园里有吃不完的苹果,求小明和小王一共最多吃多少个苹果。在这个例子中,我们可以选用的贪心策略为,每个人吃自己能吃的最多数量的苹果,这在每个人身上都是局部最优的。又因为全局结果是局部结果的简单求和,且局部结果互不相干,因此局部最优的策略也同样是全局最优的策略。
2.2 分配问题
[455. 分发饼干]
题目描述
- 有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。
输入输出样例
- 输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量。
Input: [1,2], [1,2,3] 前面数组是孩子,后面是饼干
Output: 2
在这个样例中,我们可以给两个孩子喂 [1,2]、[1,3]、[2,3] 这三种组合的任意一种。
题解
因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这个孩子。满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。
简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干,以此类推。
至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。
注意
对数组或字符串排序是常见的操作,方便之后的大小比较。
注意
在之后的讲解中,若我们谈论的是对连续空间的变量进行操作,我们并不会明确区分数组和字符串,因为他们本质上都是在连续空间上的有序变量集合。一个字符串“abc”可以被看作一个数组 [‘a’,‘b’,‘c’]。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin() , g.end());
sort(s.begin() , s.end());
int childen = 0;
int cookies = 0;
while(cookies < s.size() && childen < g.size() )
{
if(s[cookies] >= g[childen]) childen++;
cookies++;
}
return childen;
}
};
[135. 分发糖果]
题目描述
一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。
输入输出样例
输入是一个数组,表示孩子的评分。输出是最少糖果的数量。
Input: [1,0,2]
Output: 5
在这个样例中,最少的糖果分法是 [2,1,2]。
题解
做完了题目 455,你会不会认为存在比较关系的贪心策略一定需要排序或是选择?
虽然这一道题也是运用贪心策略,但我们只需要简单的两次遍历即可:把所有孩子的糖果数初始化为 1;先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。
通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一侧的大小关系。在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历更新后的结果为 [2,1,2]
class Solution {
public:
int candy(vector<int>& ratings) {
int size = ratings.size();
if(size == 1)return 1;
vector<int> num(size , 1);
for(int i = 1 ; i < size ; ++i)
{
if(ratings[i] > ratings[i - 1])
{
num[i] = num[i - 1] + 1;
}
}
for(int i = size - 1 ; i > 0 ; --i)
{
if(ratings[i] < ratings[i - 1])
{
//和上次分配的糖果进行比较,取最大值
num[i - 1] = max(num[i - 1] , num[i] + 1);
}
}
// std::accumulate 可以很方便地求和
return accumulate(num.begin() , num.end() , 0);
}
};
2.3 区间问题
[435. 无重叠区间]
题目描述
- 给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。
输入输出样例
输入是一个数组,数组由多个长度固定为 2 的数组组成,表示区间的开始和结尾。输出一个整数,表示需要移除的区间数量。
Input: [[1,2], [2,4], [1,3]]
Output: 1
在这个样例中,我们可以移除区间 [1,3],使得剩余的区间 [[1,2], [2,4]] 互不重叠。
题解
在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。
具体实现方法为,先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。我们这里使用 C++ 的 Lambda,结合 std::sort() 函数进行自定义排序。
在样例中,排序后的数组为 [[1,2], [1,3], [2,4]]。按照我们的贪心策略,首先初始化为区间[1,2];由于 [1,3] 与 [1,2] 相交,我们跳过该区间;由于 [2,4] 与 [1,2] 不相交,我们将其保留。因此最终保留的区间为 [[1,2], [2,4]]。
注意
需要根据实际情况判断按区间开头排序还是按区间结尾排序。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 1)return 0;
sort(intervals.begin() , intervals.end() , [](vector<int>& a , vector<int>& b){
return a[1] < b[1]; //按照区间第二个数来排序,越小排的越前面
});
int pre = intervals[0][1] , total = 0 ;
for(int i = 1 ; i < intervals.size() ; ++i)
{
//如果这两个有重叠,那么就略过这个元素往后继续找
if(pre > intervals[i][0]){
total++;
}
else{
//没有重叠的话就要更换pre
pre = intervals[i][1];
}
}
return total;
}
};
2.4 练习
[605. 种花问题]
题目描述
- 假设有一个很长的花坛,一部分地块种植了花,另一部分却没有,花不能种植在相邻的地块上,给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
题解
判断每个位置是否可以种花,需要当前节点,前置节点,后置节点同时为0,
另外有两个特殊节点,就是如果是第一个节点则没有前置节点(置为 0),如果是最后一个节点,那么后置节点(置为 0)。
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
for(int i = 0; i< flowerbed.size() && n > 0 ; ++i)
{
if(flowerbed[i] == 1)continue;
//到达此处意味着,上一个点为0
//这里找出这个点两旁的元素是否为0,如果都为0那么可以插入
//这里考虑到两端,一个没有前驱,一个没有后继,所以用三元运算符
int pre = (i == 0 ? 0 : flowerbed[i - 1]);
int next = (i == flowerbed.size() - 1 ? 0 : flowerbed[i + 1]);
if(pre == 0 && next ==0){
n--;
flowerbed[i] = 1;
}
}
return n == 0;
}
};
[452. 用最少数量的箭引爆气球]
题目描述
- 一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组 points ,返回引爆所有气球所必须射出的最小弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
在x = 6处射出箭,击破气球[2,8]和[1,6]。
在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
题解:
先给数组排序,这样好找出不重叠的区间
找到所有不重叠空间的数量,就是要射出剑的个数。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
//这里必须要用引用,不然会报超时
sort(points.begin() , points.end() , [](vector<int>& v1 , vector<int>& v2){
return v1[1] < v2[1];//区间第二个数小的排前面,是便于后面找是否有重叠
});
int cur = points[0][1];
int count = 1;
for(int i =1 ; i < points.size() ; ++i)
{
if(cur >= points[i][0])continue;
//若气球没有重叠,则再加一只箭
cur = points[i][1];
count++;
}
return count;
}
};
[763. 划分字母区间]
题目描述
- 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]解释:划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
题解:
分析题意:我们要找的切片片段中的字符串都是互不相交的,需要先找到S字符串中每一个字符最后一次出现的位置,并把它一一保存下来,后面再找到的划分后尽可能多的最大不相交的子区间,然后返回区间长度即可。
初始化每一个片段开始下标为start,结束下标为end,对于每一个访问到的字母S(i),得到当前字母的最后一次出现的下标位置end(i),此时我们调用max(end , end(i))进行比较,这样就能保证每次划分的最大子区间是局部最优解了。
这个主要是贪心结合双指针
注意 :
为了满足你的贪心策略,是否需要一些预处理?
在处理数组前,统计一遍信息(如频率、个数、第一次出现位置、最后一次出现位置等)可以使题目难度大幅降低。
- 这里主要是描述了第二个片段
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> ans;
vector<int> charIndex(26 , 0);
for(int i = 0; i < s.size() ; ++i){
charIndex[s[i] - 'a'] = i;
}
int start = 0, end = 0;
for(int i = 0 ; i<s.size() ; ++i){
//易错点:charIndex[s[i] - 'a']写成charIndex[0]
//因为字母是有顺序的,这里应该按字母的顺序来
end = max(charIndex[s[i] - 'a'] , end);
// 若相等,则end这个下标是之前所有字母的最大下标
// 则在这个位置可以把之前所有字母都隔离
if(end == i){
ans.push_back(end - start + 1);
start = end + 1;
}
}
return ans;
}
};
[122. 买卖股票的最佳时机 II]
题目描述
-
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
-
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
-
返回你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
题解:
首先我们是站在已经知道股票的涨幅,去卖股票的
所以可以转化为,只要股票第二天涨价,我前一天就买,第二天就卖,这样一定是赚取最多的办法
class Solution {
public:
int maxProfit(vector<int>& prices) {
int total = 0;
for(int i = 1 ; i < prices.size() ; i++){
if(prices[i] > prices[i - 1])
{
total+=(prices[i] - prices[i - 1]);
}
}
return total;
}
};