贪心
贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。
分配问题
455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
思路:
孩子和饼干分别按胃口和尺寸升序排序,按胃口匹配饼干,不足就跳过该饼干
代码:
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int j = 0, cnt = 0;
for (int i = 0; i < g.size() && j < s.size(); j++)
g[i] <= s[j] ? i++, cnt++ : true;
return cnt;
}
};
135. 分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
思路:
贪心原则,初始化所有孩子的糖果数为1,先从左向右遍历,右边比左边评分高,则更新糖果数为左边糖果数加1,再从右向左遍历,左边比右边评分高,若左边糖果数比右边高,则不必更新,反之糖果数为右边糖果数加1
代码:
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> score(ratings.size(), 1);
int cnt = 0;
for (int i = 0; i < ratings.size() - 1; i++)
ratings[i + 1] > ratings[i] ? score[i + 1] = score[i] + 1 : true;
for (int i = ratings.size() - 1; i > 0; i--) {
cnt += score[i];
(ratings[i - 1] > ratings[i]) && (score[i - 1] <= score[i]) ? score[i - 1] = score[i] + 1 : true;
}
return cnt + score[0];
}
};
区间问题
435. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
思路:
贪心原则,按结束端升序排列,从左向右遍历,若左区间与右区间重叠,抛弃右区间,比较下一个
(按结束端升序是精髓,保证先留下占最前面的,给后面预留的空间更多,得到的区间也会更多)
代码:
class Solution {
public:
static bool comp(vector<int>& a, vector<int>& b) {
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), comp);
int cnt = 0;
for (int i = 0, j = 1; i < intervals.size() && j < intervals.size();) {
if (intervals[j][0] < intervals[i][1])
j++, cnt++;
else
i = j++;
}
return cnt;
}
};
练习
605. 种花问题
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
思路:
贪心原则,从左向右遍历,若是连续的两个0,判断是否是在开头或前一个是否是0,是则令第一个为1,不是的话令第二个为1;若是连续的两个1,则证明前一次更新无效,将前一次更新恢复
(另一种简单逻辑:记录每个花的下标,从最大的开始,判断下标+2是否超出,未超出则设为1.然后继续+2判断,完了以后回到那个最大的,下标-2看看是否是0,是0的话比较第二大花的下标是否是-2后的下标或者是-3的下标,不是的话记录为1)
代码:
class Solution {
public:
int count(vector<int>& flowerbed, int first, int l) {
int cnt = 0, k = 1, si;
l > 0 ? si = flowerbed.size() - 1 : si = 0;
for (int i = first; l > 0 ? i < flowerbed.size() - 1 : i > 0; ) {
if (i + k * l == si)
break;
if (!flowerbed[i + (k + 1) * l]) {
if (i + (k + 1) * l == si) {
cnt++;
break;
}
else if (flowerbed[i + (k + 2) * l])
i += (k + 2) * l;
else
cnt++, i += (k + 1) * l;
}
else
i += (k + 1) * l;
}
return cnt;
}
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
int first = -1, cnt = 0;
for (int i = 0; i < flowerbed.size(); i++){
if (flowerbed[i] == 1) {
first = i;
break;
}
}
if (first < 0) {
for (int i = 0; i < flowerbed.size(); i += 2)
cnt++;
}
else {
cnt += count(flowerbed, first, 1);
cnt += count(flowerbed, first, -1);
}
return cnt >= n;
}
};
452. 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
思路:
贪心原则,按结束直径排序,然后按照类似435:无重叠区间的解法,倒推,因为所有气球都要被打到,这样从左到右把所有与当前区间有重叠的区间剔除,就能保证打到最后剩下的就是打出子弹最少的气球数(注意:在这个问题中上一个区间的结束和下一个区间的起始相同的话也算重叠)
(为什么这题能用无重叠区间解决呢?去掉重复的,意味着剩下的每个区间用一枪解决掉就可以干掉所有气球)
代码:
class Solution {
public:
static bool comp(vector<int>& a, vector<int>& b) {
return a[1] < b[1];
}
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), comp);
int cnt = 0;
for (int i = 0, j = 1; i < points.size() && j < points.size();) {
if (points[j][0] <= points[i][1])
j++, cnt++;
else
i = j++;
}
return points.size() - cnt;
}
};
763. 划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
思路:
先遍历一遍,记录每个字母的出现区间,按首次出现顺序排序,根据贪心原则,同一字母最多出现在一个区间中,那么这个区间就要大于其中所有字母的区间,于是从左向右遍历,从第一个字母的区间开始,如果下一个出现的字母区间开始下标小于当前区间结束下标,且结束下标大于当前区间的结束下标,则将它更新为本区间新的结束下标,直到下一个字母区间与当前无重叠时,我们就得到了一个最小区间,如:[1,5],[2,3],[4,6],[7,8],划分区间为[1,6],[7,8]
(字符只能在一个片段,那么就记录所有字符的区间,按照结束区间升序,找到一个区间的起始大于上一个的结束,证明上一个区间之前的字符串包含的字符已经不会出现在后面的区间,这就是一个字段)
代码:
class Solution {
public:
static bool comp(vector<int>& a, vector<int>& b) {
return a[0] < b[0];
}
vector<int> partitionLabels(string s) {
int cnt = 0;
vector<vector<int>> n;
vector<int> re, hash(26, -1);
int first = 0, last = 0;
for (int i = 0; i < s.size(); i++) {
if (hash[s[i] - 'a'] == -1) {
hash[s[i] - 'a'] = cnt++;
n.push_back({ i,i });
}
else
n[hash[s[i] - 'a']][1] = i;
}
sort(n.begin(), n.end(), comp);
first = n[0][0], last = n[0][1];
for (int i = 0, j = 1; i < n.size() && j < n.size();j++) {
if (n[j][0] > last)
re.push_back(last - first + 1), i = j, first = n[i][0], last = n[i][1];
else if (n[j][1] > last)
last = n[j][1];
}
re.push_back(last - first + 1);
return re;
}
};
122. 买卖股票的最佳时机 II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路:
贪心原则,低价买高价卖,从左向右遍历,当价格连续下降时,时刻记录最低价,一旦停止下跌,在最低价买入,当价格不断升高时,时刻记录最高价,一旦开始下跌,在最高价抛出
(要求利润多,而且不限制交易次数,那么就是尽量低买高卖,如果股价递增,就头天买,谷峰卖,递减的话就在谷底买)
代码:
class Solution {
public:
int ftop(vector<int>& prices,int n, int pos) {
for (int i = n; i < prices.size()-1; i++) {
if (pos == 1 ? prices[i] > prices[i + 1] : prices[i] < prices[i + 1])
return i;
}
return prices.size()-1;
}
int maxProfit(vector<int>& prices) {
int sum = 0;
for (int i = 0; i < prices.size() - 1; i++) {
if (prices[i] < prices[i + 1])
sum += (-prices[i] + prices[i = ftop(prices, i + 1, 1)]);
else
i = ftop(prices, i + 1, -1) - 1;
}
return sum;
}
};
406. 根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
思路:
按身高优先降序处理,再按第二项系数升序处理,出于贪心原则,优先处理身高高的,比他高的人就更少,插入队列操作就更方便,第二项升序亦是如此,这样,第i个处理的人身前一定有i个比他高的人,如果i小于他的第二项系数,那么就插入到新队列的队尾,否则插入到新队列中第i个,这样,后来插入的人的身高都小于等于他,第二项系数也大于等于他,不会影响到他,这样排序就极为方便了
(双属性一般按两个属性排,像这种情况下,肯定是先按身高降序,因为第二项属性是比自己高的,那么先处理身高高的肯定要好排一点,然后第二项属性自然是升序,因为比自己高的越少,也是越好排,那么这么一排以后,发现每次处理的人都小于等于已经处理的人的身高,那么有几个人比他高就插在第几个就行了,这里为方便,使用其他数据结构,然后转为数组输出)
代码:
class Solution {
public:
static bool comp(vector<int>& a, vector<int>& b) {
return a[0] == b[0] ? a[1] < b[1]:a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
vector<vector<int>> re;
sort(people.begin(), people.end(), comp);
for (int i = 0; i < people.size();i++) {
if (people[i][1] < re.size())
re.insert(re.begin() + people[i][1],people[i]);
else
re.push_back(people[i]);
}
return re;
}
};
665. 非递减数列
给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。
思路:
根据题意,非递减序列定义为nums[i] <= nums[i + 1],那么当序列在i处出现下降时(nums[i] > nums[i + 1]),如果nums[i] > nums[i + 1],使nums[i] = nums[i - 1],其他情况修改nums[i + 1] = nums[i],这样根据贪心原则,下降处之前都能保持非递减,当修改大于一次,则返回FALSE
(ps:特殊情况:i=0时直接修改nums[i] = nums[i + 1])
(这种题就是遍历呗,出现第一个不符的,判断它和前前一个是否非递减,是就把它的前一个降到和自己一个水平,不是的话自己就升到前一个的水平,这样是最贪心的改法,因为如果是递减的,你改前一个也没有用,非递减的话只改前一个对后面影响最小)
代码:
class Solution {
public:
bool checkPossibility(vector<int>& nums) {
int cnt = 0;
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i] > nums[i + 1]) {
if (!i || nums[i + 1] > nums[i - 1])
nums[i] = nums[i+1];
else if (nums[i + 1] < nums[i - 1])
nums[i + 1] = nums[i];
if (++cnt > 1)
return false;
}
}
return true;
}
};