贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
(来源:链接:https://leetcode-cn.com/tag/greedy/)
贪心算法一般用来解决需要 “找到要做某事的最小数量” 或 “找到在某些情况下适合的最大物品数量” 的问题,且提供的是无序的输入。
贪心算法的思想是每一步都选择最佳解决方案,最终获得全局最佳的解决方案
标准解决方案具有 O(NlogN) 的时间复杂度且由以下两部分组成:
- 思考如何排序输入数据(O(NlogN) 的时间复杂度)。
- 思考如何解析排序后的数据(O(N) 的时间复杂度)
如果输入数据本身有序,则我们不需要进行排序,那么该贪心算法具有 O(N) 的时间复杂度。
如何证明你的贪心思想具有全局最优的效果:可以使用反证法来证明。
(来源:作者:LeetCode链接:https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/solution/yong-zui-shao-shu-liang-de-jian-yin-bao-qi-qiu-b-2/)
目录
常见问题
https://leetcode-cn.com/problems/assign-cookies/
贪心的问题大都没有套路可循,而且形式上贴近DP和回溯
唯一区别就是不需要列举所有的情况,只需要找到每步的最优解,即可完成任务
本题目要求:满足越多的孩子越好,那么我们直接将孩子胃口和饼干尺寸升序排列
static int cmp (const void *s1, const void *s2) {
return *(int *)s1 > *(int *)s2 ? 1 : -1;
}
int findContentChildren(int* g, int gSize, int* s, int sSize){
// 贪心的问题大都没有套路可循,而且形式上贴近DP和回溯
// 唯一区别就是不需要列举所有的情况,只需要找到每步的最优解,即可完成任务
// 本题目要求:满足越多的孩子越好,那么我们直接将孩子胃口和饼干尺寸升序排列
qsort(g, gSize, sizeof(g[0]), cmp);
qsort(s, sSize, sizeof(s[0]), cmp);
int count = 0;
int gIndex = 0;
int sIndex = 0;
while (gIndex < gSize && sIndex < sSize) {
// 找到合适孩子胃口的饼干尺寸, 然后增加满足孩子的个数
while (sIndex < sSize && s[sIndex] < g[gIndex]) {
sIndex++;
}
// 一旦超出范围,直接返回,说明以目前孩子的胃口,没有饼干的尺寸可以满足
if (sIndex >= sSize) {
break;
}
count++;
sIndex++;
gIndex++;
}
return count;
}
https://leetcode-cn.com/problems/lemonade-change/
每次都需要找零钱,金额有三种,5,10,20,而且在找零过程中,20的面额根本用不上,那么我只需要记录5和10的面额即可
bool lemonadeChange(int* bills, int billsSize){
if (bills == NULL) {
return false;
}
int five = 0;
int ten = 0;
for (int i = 0; i< billsSize; ++i) {
if (bills[i] == 5) {
five++; // 5元刚好是饮料的价格,无需找钱
} else if (bills[i] == 10) {
if (five > 0) {
// 能找钱
ten++;
five--;
} else {
// 无钱可找,那么直接返回false
return false;
}
} else {
if (five > 0 && ten > 0) {
five--;
ten--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
区间类问题(定一动一)
【begin,end】贪心算法中的区间类问题,精髓就是定一个点动一个点,想办法让begin固定,移动end来比较判断,或者想办法让end固定,依靠begin来判断
合集:https://mp.weixin.qq.com/s/ioUlNa4ZToCrun3qb4y4Ow
435. 无重叠区间
https://leetcode-cn.com/problems/non-overlapping-intervals/
区间问题:通用做法就是按照begin或者end进行升序排序,然后更加end或begin进行后续判断
题目要求去除区间的个数要尽可能的少,我们以end作为排序标准,升序排列,可以理解成你要在begin~end这个区间内开会
做多能参加多少个会议,那么显然,我们需要尽可能的参考结束早的会议(end小)
static int cmp (const void *s1, const void *s2) {
int *arg1 = *(int **)s1;
int *arg2 = *(int **)s2;
if (arg1[1] != arg2[1]) {
return arg1[1] > arg2[1] ? 1 : -1;
} else {
return arg1[0] > arg2[0] ? 1 : -1;
}
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize){
// 题目要求移除区间的个数,本质还是找重叠区间的个数
if (intervals == NULL) {
return 0;
}
qsort(intervals, intervalsSize, sizeof(intervals[0]), cmp);
int rear = intervals[0][1];
int count = 1;
for (int i = 1; i < intervalsSize; ++i) {
if (rear <= intervals[i][0]) {
rear = intervals[i][1];
count++; // 记录最大不重叠的区间(最多能参加的会议,)
}
}
return intervalsSize - count;
}
完成上面的题目后,下面的252就很好理解了
252. 会议室
https://leetcode-cn.com/problems/meeting-rooms/
static int cmp (const void *s1, const void *s2) {
int *arg1 = *(int **)s1;
int *arg2 = *(int **)s2;
if (arg1[1] != arg2[1]) {
return arg1[1] > arg2[1] ? 1 : -1;
} else {
return arg1[0] > arg2[0] ? 1 : -1;
}
}
bool canAttendMeetings(int** intervals, int intervalsSize, int* intervalsColSize){
if (intervals == NULL || intervalsSize == 0) {
return true;
}
qsort(intervals, intervalsSize, sizeof(intervals), cmp);
int count = 1;
int rear = intervals[0][1];
for (int i = 1; i < intervalsSize; ++i) {
if (rear <= intervals[i][0]) {
rear = intervals[i][1];
count++;
}
}
return count == intervalsSize ? true : false;
}
452. 用最少数量的箭引爆气球
https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/
从直觉来看,本题是在找重叠区间的个数,还是定一个动一个,问题在于我们选择哪儿个定,哪儿个动
比起大气球(区间),我们更加关注小气球(区间)
所以那end点升序,那begin点判断,如果重叠,那么就是一根弓箭如果不重叠,就是两根
static int cmp (const void *s1, const void *s2) {
int *arg1 = *(int **)s1;
int *arg2 = *(int **)s2;
if (arg1[1] != arg2[1]) {
return arg1[1] > arg2[1] ? 1 : -1;
}
return arg1[0] > arg2[0] ? 1 : -1;
}
int findMinArrowShots(int** points, int pointsSize, int* pointsColSize){
if (points == NULL) {
return 0;
}
qsort(points, pointsSize, sizeof(points), cmp);
int rear = points[0][1];
int count = 1;
for (int i = 1; i < pointsSize; ++i) {
if (rear < points[i][0]) {
rear = points[i][1];
count++;
}
}
return count;
}
56. 合并区间
https://leetcode-cn.com/problems/merge-intervals/
区间类问题:定一个点动一个点
如果固定end点,那么begin的位置没办法保证,我们在合并区间的时候,如果出现了end0 >= begin1的情况,那么我们还要比较begin1和begin0的大小
选择小的那个作为区间的begin位置,begin和end两个位置都在参加比较,不合适
如果选择固定begin点,那么过程就会轻松很多,每次比endi和begini+1即可,因为是按照begin进行顺序排列的,不用担心begin是不是最小,一定是最小
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
static int cmp (const void *s1, const void *s2) {
int *arg1 = *(int **)s1;
int *arg2 = *(int **)s2;
if (arg1[0] != arg2[0]) {
return arg1[0] > arg2[0] ? 1 : -1;
}
return arg1[1] > arg2[1] ? 1 : -1;
}
int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes){
if (intervals == NULL) {
return NULL;
}
qsort(intervals, intervalsSize, sizeof(intervals[0]), cmp);
int **res = (int **)malloc(sizeof(int *) * intervalsSize);
for (int i = 0;i < intervalsSize; ++i) {
res[i] = (int *)malloc(sizeof(int) * intervalsColSize[0]);
memset(res[i], 0, sizeof(int) * intervalsColSize[0]);
}
// 注意合并区间的技巧,先放一个元素进入区间,然后比较end0和begin1,如果有需要,那么直接更新end即可,比较有技巧的一种写法
int index = 0;
res[index][0] = intervals[0][0];
res[index][1] = intervals[0][1];
int rear = intervals[0][1];
for (int i = 1; i < intervalsSize; ++i) {
if (rear >= intervals[i][0]) {
rear = fmax(rear, intervals[i][1]);
res[index][1] = rear; // 随时需要更新
} else {
rear = intervals[i][1]; // 随时需要更新
res[++index][0] = intervals[i][0];
res[index][1] = intervals[i][1];
}
}
returnSize[0] = index + 1; // 次数给的是区间的个数 需要下标加1,这也和index自增的位置有很大的关系
returnColumnSizes[0] = (int *)malloc(sizeof(int) * returnSize[0]);
for (int i = 0;i < returnSize[0]; ++i) {
returnColumnSizes[0][i] = intervalsColSize[0];
}
return res;
}
57. 插入区间
https://leetcode-cn.com/problems/insert-interval/
本题需要固定起点,拿终点去比较才行,不断在循环中更新区间的终点
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
#define SIZE 2
static int cmp (const void *s1, const void *s2) {
int *arg1 = *(int **)s1;
int *arg2 = *(int **)s2;
if (arg1[0] != arg2[0]) {
return arg1[0] > arg2[0] ? 1 : -1;
}
return arg1[1] > arg2[1] ? 1 : -1;
}
int** insert(int** intervals, int intervalsSize, int* intervalsColSize, int* newInterval, int newIntervalSize, int* returnSize, int** returnColumnSizes){
(*returnSize) = 0;
int **res = (int **)malloc(sizeof(int *) * (intervalsSize + 1));
for (int i = 0; i < intervalsSize + 1; ++i) {
res[i] = (int *)malloc(sizeof(int) * SIZE);
memset(res[i], 0, sizeof(int) * SIZE);
}
for (int i = 0; i < intervalsSize; ++i) {
res[(*returnSize)][0] = intervals[i][0];
res[(*returnSize)++][1] = intervals[i][1];
}
res[(*returnSize)][0] = newInterval[0];
res[(*returnSize)++][1] = newInterval[1];
qsort(res, intervalsSize + 1, sizeof(res[0]), cmp);
int index = 0;
int rear = res[0][1];
int **add = (int **)malloc(sizeof(int *) * (*returnSize));
add[0] = (int *)malloc(sizeof(int) * SIZE);
add[0][0] = res[0][0];
add[0][1] = res[0][1];
for (int i = 1; i < (*returnSize);++i) {
if (rear < res[i][0]) {
// 插入当前区间
rear = res[i][1];
index++;
add[index] = (int *)malloc(sizeof(int) * SIZE);
add[index][0] = res[i][0];
add[index][1] = res[i][1];
} else {
rear = fmax(rear, res[i][1]);
add[index][1] = rear;
}
}
returnSize[0] = index == 0 ? 1 : index + 1;
returnColumnSizes[0] = (int *)malloc(sizeof(int) * (*returnSize));
for (int i = 0; i < (*returnSize); ++i) {
returnColumnSizes[0][i] = SIZE;
}
return add;
}
跳跃问题
55. 跳跃游戏
https://leetcode-cn.com/problems/jump-game/
class Solution {
public:
bool canJump(vector<int>& nums) {
if(nums.empty()) return true;
int size = nums.size();
if(size == 1) return true;
int begin = 0,end = 0;
while(begin<size-1)//不能等于最后一个
{
if(nums[begin] == 0&&end<=begin)//不能跳
return false;
end = max(end,begin+nums[begin]);
begin++;
}
return end>=size-1?true:false;
}
};
45. 跳跃游戏 II
https://leetcode-cn.com/problems/jump-game-ii/
https://leetcode-cn.com/problems/jump-game-ii/solution/45-by-ikaruga/
class Solution {
public:
int jump(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
int begin = 0,end = 1,MAXpos = 0,step = 0;
while(end<size)
{
for(int i = begin;i<end;++i)
{
MAXpos = max(MAXpos,i+nums[i]);
}
begin = end;
end = MAXpos+1;
step++;
}
return step;
}
};
版本2:
class Solution {
public:
int jump(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
int begin = 0,end = 1,step= 0;
while(end<size)
{
int MAX = 0;
for(int j = begin+1;j<size&&j<end;++j)
{
if(j+nums[j]>=MAX) {MAX = j+nums[j];begin = j;}
}
step++;
int length = nums[begin];//目前能达到最长的位置
end = begin+length+1;
}
return step;
}
};
本题难度不在于想法,而在于怎么落实。
每次能走都远走多远,尽量去走。这就是核心思想,但是代码落实就会出现很多的问题。
其他问题
605. 种花问题
https://leetcode-cn.com/problems/can-place-flowers/
class Solution {
public:
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
int size = flowerbed.size();
if(size == 0) return 0;
int Pre = 0;
int Left = 0,right = Pre+1;
while(Pre<size)
{
if(flowerbed[Pre] == 1)//当前是1
Pre+=2;
else
{
// cout<<Pre<<endl;
if(right>size-1&&flowerbed[Left] == 0)
{
n--;break;
}
else if(right>size-1&&flowerbed[Left] == 1) break;
else if(right<=size-1&&flowerbed[Left] == 0&&flowerbed[right] == 0)
{
n--;
Pre+=2;
}
else if(flowerbed[Left] == 1) Pre++;
else if(right<=size-1&&flowerbed[right] == 1) Pre = right+2;
}
Left = Pre-1,right = Pre+1;
}
if(n <= 0) return true;
else return false;
}
};
376. 摆动序列
https://leetcode-cn.com/problems/wiggle-subsequence/
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
if(size == 1) return 1;
// if(size == 2&&nums[0] == nums[1]) return 1;
vector<int> Sample;
for(int i = 0;i<size-1;++i)
{
int temp = nums[i]-nums[i+1];
if(temp<0)
Sample.push_back(0);//负数
else if(temp>0) Sample.push_back(1);//正数
}
if(Sample.size() == 0) return 1;
int Next = (Sample[0]==0?1:0),Res = 1;
for(int i = 1;i<Sample.size();++i)
{
if(Sample[i] == Next) {Res++;Next = (Sample[i]==0?1:0);}
}
return Res+1;
}
};
402. 移掉K位数字
https://leetcode-cn.com/problems/remove-k-digits/
// 执行用时 : 0 ms, 在所有 C++ 提交中击败了 100%的用户
// 内存消耗 : 9.5 MB, 在所有 C++ 提交中击败了 31.19%的用户
class Solution {
public:
string removeKdigits(string num, int k) {
if(num.size() == k) return string(1, '0');
string stk;
int i = 0;
while(k > 0 && i < num.size()) // 将num中的字符按规则移动到栈中 栈保存遍历过的单调不减的元素
{
if(stk.empty() || stk.back() <= num[i]) // 直接入栈,并转而遍历下一个元素
{
stk.push_back(num[i]);
++i;
}
else // stk.back() > num[i]
{
stk.pop_back();
--k;
}
}
// 1. 如果i == 0, 则 k 可能不等于0, 移除掉stk末尾k个元素.
// 2. 如果k == 0, 则 i 可能不等于0, 需要加上num中i之后的元素.
stk = stk.substr(0, stk.size() - k) + num.substr(i);
// 移除开头的0,在全0的情况下保证至少剩下一个0.
size_t beginIndex = 0;
while(beginIndex < stk.size() - 1 && stk[beginIndex] == '0') ++beginIndex;
return stk.substr(beginIndex);
}
};
// 作者:fu-guang
// 链接:https://leetcode-cn.com/problems/remove-k-digits/solution/c-0ms-ji-bai-100xiang-xi-si-lu-xi-jie-jie-xi-by-fu/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
面试题66. 构建乘积数组
https://leetcode-cn.com/problems/gou-jian-cheng-ji-shu-zu-lcof/
238. 除自身以外数组的乘积 https://leetcode-cn.com/problems/product-of-array-except-self/
本题初见,题意不是很好理解
其实就是从i开始算,i左侧全部元素的乘积与右侧全部元素的乘积再次做乘法
那么我们很容易发现规律:
前缀部分乘积也是一样的,计算完前后缀乘积后,二者再次相乘,即可完成运算。
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
int size = a.size();
vector<int>B(size,1);
for(int i = size-1;i>0;--i)
{
B[i-1] = a[i]*B[i];
}
vector<int>A(size,1);
for(int i = 0;i<size-1;++i)
{
A[i+1] = a[i]*A[i];
}
vector<int>Res;
for(int i = 0;i<=size-1;++i)
{
Res.push_back(A[i]*B[i]);
}
return Res;
}
};
有没有更好的办法,降低空间复杂度?
因为输出数组不算复杂度,那么我们用输出数组来代替上面的左右两个数组
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
if(nums.empty()) return {};
int size = nums.size();
vector<int> Res(size,1);
Res[0] = 1;
for(int i = 1;i<size;++i)
{
Res[i] = Res[i-1]*nums[i-1]; //用保存结果的容器,先保存左侧乘积
}
//左侧乘积我们算了,下面就是右侧乘积的事儿了
//显然我们不能从头算,我们从后往前算
int R = 1;
for(int i = size - 1;i>=0;--i)
{
Res[i] *= R;
R *= nums[i];
}
return Res;
}
};
https://leetcode-cn.com/problems/group-anagrams/
本题核心内容是:就算字符串不同,但是组成该字符串的字母,都一样的,那么我们对字符串进行排序,不管原来字符串中的字母是怎样组合的,现在都一样了。
然后我们需要知道怎么将这些内容插入到一个一维数组中去
我们使用hash表,index是字符串,value是该字符串在Res二维数组的索引下标
Hash表现查找,有没有这个内容,有的话,就直接将该内容插入到该插入的位置
Hash表没有找到,那么说明第一次出现这个字符串,那么我们规定其下标,同时创建一个一维数组,插入二维数组中
整个过程非常巧妙:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
if(strs.empty()) return {};
vector<vector<string>>Res;
int index = 0;//不同数字的下标
unordered_map<string,int>M;//int部分是该组合全部字符传在res中的索引
for(auto item:strs)
{
string temp = item;
sort(temp.begin(),temp.end());
cout<<item<<endl;
if(M.find(temp)!=M.end()) Res[M[temp]].push_back(item);
//找到了,那么就按照索引插入到结果中
else //只有遇到新的字符串,才会进入这个部分,创建一维数组插入到二维数组中,同时记下该类字符串的索引
{
vector<string> vec(1,item);//新建一维数组,并插入结构
Res.push_back(vec);
M[temp] = index;//标记该字符串的索引(从0开始)
index++;
}
}
return Res;
}
};