贪心算法或贪心思想,即采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。以下是我在leetcode上面做过的一些经典贪心算法类型题,在此将解题思路分享出来,如有不正确的地方欢迎指正。
有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子可以吃饱。
输入两个数组,分别代表孩子的饥饿度和饼干的大小。输出最多有多少孩子可以吃饱的数量。
Input: [1,2], [1,2,3]
Output: 2
因为饥饿度最小的孩子最容易吃饱,所以我们先考虑这个孩子。为了尽量使得剩下的饼干可以满足饥饿度更大的孩子,所以我们应该把大于等于这个孩子饥饿度的、且大小最小的饼干给这个孩子。满足了这个孩子之后,我们采取同样的策略,考虑剩下孩子里饥饿度最小的孩子,直到没有满足条件的饼干存在。
简而言之,这里的贪心策略是,给剩余孩子里最小饥饿度的孩子分配最小的能饱腹的饼干。
至于具体实现,因为我们需要获得大小关系,一个便捷的方法就是把孩子和饼干分别排序。这样我们就可以从饥饿度最小的孩子和大小最小的饼干出发,计算有多少个对子可以满足条件。
int findContentChildren(vector<int>& children, vector<int>& cookies) {
sort(children.begin(), children.end());
sort(cookies.begin(), cookies.end());
int child = 0, cookie = 0;
while (child < children.size() && cookie < cookies.size()) {
if (children[child] <= cookies[cookie]) ++child;
++cookie;
}
return child;
}
一群孩子站成一排,每一个孩子有自己的评分。现在需要给这些孩子发糖果,规则是如果一个孩子的评分比自己身旁的一个孩子要高,那么这个孩子就必须得到比身旁孩子更多的糖果;所有孩子至少要有一个糖果。求解最少需要多少个糖果。
输入是一个数组,表示孩子的评分。输出是最少糖果的数量。
Input: [1,0,2]
Output: 5
在这个样例中,最少的糖果分法是 [2,1,2]。
做完了题目 455,你会不会认为存在比较关系的贪心策略一定需要排序或是选择?虽然这一道题也是运用贪心策略,但我们只需要简单的两次遍历即可:把所有孩子的糖果数初始化为 1;先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。通过这两次遍历,分配的糖果就可以满足题目要求了。这里的贪心策略即为,在每次遍历中,只考虑并更新相邻一侧的大小关系。
在样例中,我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历更新后的结果为 [2,1,2]。
int candy(vector<int>& ratings) {
int size = ratings.size();
if (size < 2) {
return size;
}
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);
} }
return accumulate(num.begin(), num.end(), 0); // std::accumulate 可以很方便 地求和 }
给定多个区间,计算让这些区间互不重叠所需要移除区间的最少个数。起止相连不算重叠。
输入是一个数组,数组由多个长度固定为 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]]。
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.empty()) {
return 0;
}
int n = intervals.size();
sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b)
{
return a[1] < b[1];
});
int removed = 0, prev = intervals[0][1];
for (int i = 1; i < n; ++i) {
if (intervals[i][0] < prev) {
++removed;
} else {
prev = intervals[i][1];
} }
return removed;
}
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
input:flowerbed = [1,0,0,0,1], n = 1
output:true
input:flowerbed = [1,0,0,0,1], n = 2
output:false
这道题可以直接使用数学归纳法进行解决。
通过分析可以得出,若一串连续的0的个数为某个数,对应的能够种植花的个数为:
1~2:0
3~4:1
5~6:3
…以此类推,可以得出,种植花的个数=(zerocount-1)/2。
但当处在开始位置和结束位置的时候,由于有一边的花不用考虑,得到的公式与上面不大一样,这时可以在开始位置的左边手动添加一个0,在结束位置的右边手动添加一个0,表示该0对花的种植情况无影响,这样得到的公式便和上面的公式统一了。
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
int num=flowerbed.size();
int zerocount=1;
int sum=0;
for(int i=0;i<num;i++)
{
if(flowerbed[i]==0)
zerocount+=1;
else{
sum+=(zerocount-1)/2;
zerocount=0;
}
}
zerocount+=1;
sum+=(zerocount-1)/2;
if(sum>=n)
return true;
else
return false;
}
};
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
input:points = [[10,16],[2,8],[1,6],[7,12]]
output:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球
input:points = [[1,2],[3,4],[5,6],[7,8]]
output:4
解释:由于这些区间互相之间不重叠且边界无相连,故每一个区间都需要一支箭来引爆。
这道题的思路与435题较为相似,仍然是先按数组的后一项从小到大排序,然后每一次让箭从该数组后边界射出(这里体现了贪心思想,目的是让付出代价不变的情况下,能够射中更多的气球)。然后依次循环,判断该箭是否能够射中剩下的气球,若不能则将箭的数量加一。以此类推。
这道题若想得到更为详细的解答,可以去leetcode上看官方题解。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(),points.end(),[](vector<int> &a,vector<int> &b){return a[1]<b[1];});
int i=1;
int num=points.size();
int prev=points[0][1];
for(int j=0;j<num;j++){
if(points[j][0]>prev){
i++;
prev=points[j][1];
}
}
return i;
}
};
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
为了保证每个字母最多出现在一个片段里,需要记录每个字母第一次和最后一次出现的位置,这样才能帮助设置每个片段包含了字母的第一次出现和最后一次出现。之后遍历字符串,设置一个start和end变量,表示该划分的子串的初始位置和结尾位置,初始值自然都为0;每次遍历时,令end=max(end,该字母对应的最后位置);当遍历到end时,即表示该子字符串已经遍历结束,此时便在结果列表中记录子字符串的长度,然后开始下一次的寻找,直至字符串的末尾。
class Solution {
public:
vector<int> partitionLabels(string S) {
int num[26];
int len=S.size();
int i;
for(i=0;i<len;i++){
num[S[i]-'a']=i;
}#记录字符串中26个字母最后一次出现的位置
int start=0,end=0;
vector<int> result;
for(i=0;i<len;i++){
end=max(end,num[S[i]-'a']);
if(i==end){
result.push_back(end-start+1);
start=end+1;
}
}
return result;
}
};
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为0。
这道题就是最简单的贪心算法,因为交易次数没有限制,所以只需要把所有的上升区间加起来就可以了,这样利益就一定能最大化,哈哈想不到吧!
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len=prices.size();
int i,sum=0;
for(i=1;i<len;i++){
sum+=max(0,prices[i]-prices[i-1]);
}
return sum;
}
};