贪心题集

摆动序列

. - 力扣(LeetCode)

这道题就是要求给定的数组中的峰值数以及端点。

把数组抽象成折线图会好理解一些,那么根据题意,我们就需要判断这些相邻的数的差值,不妨设pre_diff = nums[i] - nums[i - 1],cur_diff = nums[i + 1] - nums[i]。

那么出现峰值的条件为

if((pre_diff < 0 && cur_diff > 0) || (pre_diff > 0 && cur_diff < 0))

那么就需要三个数来计算,但是数组假如就只有两个数呢?

我们在数组的第一个元素之前引入一个相同的数,所以在开始状态pre_diff = 0。那么就需要修改判断峰值条件了

if((pre_diff <= 0 && cur_diff > 0) || (pre_diff >= 0 && cur_diff < 0))

这样就包括了只有两个元素的情况。

还需要注意两个情况:

1、上下坡中有平坡

2、单调坡中有平坡

最大子数组和

. - 力扣(LeetCode)

这道题的暴力方法就是两个for循环,代码如下

int maxSubArray(int* nums, int numsSize) {
    int result = INT_MIN;
    for(int i = 0; i < numsSize; i++){
        int count  = 0;
        for(int j = i; j < numsSize; j++){
            count += nums[j];
            result = result > count ? result : count;
        }
    }
    return result;
}

暴力方法明显时间复杂度是O(n^2)。使用C语言的暴力在leetcode是c不了的,会超时。

那么就需要换一种想法:贪心。 

这道题很全面的体现了贪心的贪。这道题的题意是求给定的数组中最大的子数组和。

而贪心贪的点就在于:要想子数组和最大,那么一定要保证子数组和大于0。(当然,若数组中的所有数都小于0,那么这就不成立了,对于这种情况,需要特别分析,后面会分析到)。

先上代码

int maxSubArray(int* nums, int numsSize) {
    int count = 0;
    int res = INT_MIN;
    for(int i = 0; i < numsSize; i++){
        count += nums[i];
        res = res > count ? res : count;
        if(count <= 0){
            count = 0;
            continue;
        }
    }
    return res;
}

首先要明确的就是这道题目贪心的点,子数组和一定需要大于0,若小于0则会拖累整个子数组的和,需要舍弃。那么解释一下上面说到的,假如全数组都是小于0的呢?那么这样得到的子数组将会是一个元素,这个元素会是该数组中最大的值。比如[-1,-2,-3],最后返回的值会是-1。

有一个误区就是,遇到负数就舍弃,其实不能舍弃,假如遇到负数,但是该子数组和还是正数,那么对后面的元素还是有利的。

拓展:这只是求子数组和的最大值。那么假如要求子数组和最大的起始位置和结束位置呢?(也就是返回最大子数组)。其实只要改动上面的代码即可。这个最大子数组的起始位置就是:每次count小于0,之后的数组下标。结束位置是:result得到最大值的位置。只需要增加两个变量即可。

买股票的最大利润

. - 力扣(LeetCode)

拿到这题首先想到的是:选择一个最低的价格买入,然后选择一个高的卖出,一直反复……

但是这样的话,好像无法区分出,哪天才是最高的,怎么卖才利润最大。

要是改变一下思路,其实得到的利润可以分解,然后将分解的正利润相加,那么就可以得到最大利润了。那么如何分解呢?

假设第一天买入,第四天卖出。那么利润是prices[3] - prices[0]。(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0]) = prices[3] - prices[0](不管是正是负,都是利润)

下面我们模拟一下

我们分解了每一天得到的利润,那么为什么第一天没有利润呢?因为第一天只能买入,不能卖出。用这个例子分析一下分解的利润相加是否可以得到总的利润。prices[3] - prices[0]  = 3。分解之后相加是:-6 + 4 + 5 = 3。

所以不用考虑最小价格买入,最大价格卖出的问题,直接分解为每天的利润,然后加上正利润就是最大的利润了。(可以抽象理解为:你买一次卖一次获得的利润,我可以买多次卖多次,只要保证每次都是赚的,也能获得和你一样的利润。)

代码奉上

int maxProfit(int* prices, int pricesSize) {
    int result = 0;
    for(int i = 1; i < pricesSize; i++){
        if((prices[i] - prices[i - 1]) > 0){
            result += prices[i] - prices[i - 1];
        }
    }
    return result;
}

跳跃游戏

. - 力扣(LeetCode)

这道题也可以不用贪心。

题目要求是从第一个跳到最后一个,那么判断从最后一个到第一个也可以得到答案。

直接先放码

bool canJump(int* nums, int numsSize) {
    int flag = numsSize - 1;
    for(int i = numsSize - 2; i >= 0; i--){
        if(i + nums[i] >= flag){
            flag = i;
        }
    }
    return flag == 0;
}

那为什么不能从前面判断呢。试试就逝世。上图,就知道原因了。

从前往后遍历,得到的是最从任意位置跳,最大可以跳到得位置。对于第一个位置开始,是充耳不闻。所以得从后往前遍历。

那么能否可贪?

当然可以。并且效率更高,后面会分析。每一个元素可以选择跳跃的距离,所以到底是选择跳多少步呢?难以判断。换个思路:计算最大可以跳到的位置

上图:

每次求最大的覆盖范围,只要在可以覆盖的地方跳跃,取最大覆盖的范围。这样就把题目转换为求最大覆盖范围是否能覆盖最后一个下标。

上码上码:

bool canJump(int* nums, int numsSize) {
    if(numsSize == 1){
        return true;
    }
    int cover = 0;
    for(int i = 0; i <= cover; i++){
        cover = (nums[i] + i) > cover ? (nums[i] + i) : cover;
        if(cover >= numsSize - 1){
        return true;
    }
    }
    
    return false;
}

注意:1、i的结束范围是小于等于cover,cover表示覆盖的范围,也就是能跳跃的最大位置,只能在这些位置里面跳跃。

           2、判断ture的条件必须在循环里面,不然会产生数组越界。(heap overflow)

跳跃游戏II

. - 力扣(LeetCode)

上一题是判断是否能到达最后一个下标。而这一题是求最小到达的跳跃次数。

那么问题来了,每一个可以有很多选择,那么是选择最大的步数跳跃吗?显然不是,例如数组:[2,3,1,1,4]。假如每次选择跳跃最大步数,那么达到最后一个下标需要3步,但是假如先跳一步,然后跳三步,这样就只需要两步了。

模拟:第一个位置可以跳到的最远位置,如果跳不到,那么下一次跳的位置应该是这一次覆盖的位置的最大位置。

#define max(a, b) (((a) > (b)) ? (a) : (b))
int jump(int* nums, int numsSize) {
    if(numsSize == 1){
        return 0;
    }
    int cur = 0, next = 0;
    int result = 0;
    for(int i = 0; i < numsSize; i++){
        next = max(nums[i] + i, next);
        if(i == cur){
            result++;
            cur = next;
            if(cur >= numsSize - 1){
                break;
            }
        }
    }
    return result;
}

k次取反后最大化的数组

. - 力扣(LeetCode)

这道题看起来很简单想到贪心的点,但是还是有一些细节需要注意。

这道题需要进行两次贪心,要想保证数组和最大,首先需要把数组中的负数变为正数。

先上代码

int cmp(const void* e1, const void* e2){
    return *(int*)e1 - *(int*)e2;
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
    qsort(nums, numsSize, sizeof(int), cmp);
    for(int i = 0; i < numsSize; i++){
        if(nums[i] < 0 && k > 0){
            nums[i] = -1 * nums[i];
            k--;
        }
        if(k == 0){
            break;
        }
    }
    if(k % 2 == 1){
        qsort(nums, numsSize, sizeof(int), cmp);
        nums[0] = -1 * nums[0];
    }
    int result = 0;
    for(int i = 0; i < numsSize; i++){
        result += nums[i];
    }
    return result;
}

先将数组从小到大排序,然后从前往后遍历,若遇到小于0的数,这样先遇到的负数也是负得最多的,这样先相反这个数,那可以保证对数组最后的和最有利的。

还有一种情况,当数组中的负数全部逆转,那么数组中全是正数,然后k还有剩,这样先判断k的奇偶性,如果是奇数那么需要把最小的正数转为负数,这样才能保证对最后数组和的减小最小,若是偶数,就不要管了,因为取反偶数次,不会有正负改变。

但是找到最小的正数,需要再次排列数组。还有一种方法只需要排列一次

对绝对值从小到大排列

这样首先从后到前遍历,遇到的负数,一定是最小的负数,对它* -1即可。遍历完之后,k还有剩余,就只需要对第一个元素处理即可

上码

int cmp(const void* e1, const void* e2){
    return abs(*(int*)e1) - abs(*(int*)e2);
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
    qsort(nums, numsSize, sizeof(int), cmp);
    for(int i = numsSize - 1; i >= 0; i--){
        if(nums[i] < 0 && k > 0){
            nums[i] = -1 * nums[i];
            k--;
        }
    }
    if(k % 2 == 1){
         nums[0] *= -1;
    }
    int result = 0;
    for(int i = 0; i < numsSize; i++){
        result += nums[i];
    }
    return result;
}

加油站

. - 力扣(LeetCode)

本题贪心的点在于找到油箱剩余的油小于0。那么起点就是下一个站点。

上图说明原因。

上码

int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
    int result = 0, total = 0, rest = 0;
    for(int i = 0; i < gasSize; i++){
        rest += gas[i] - cost[i];
        total += gas[i] - cost[i];
        if(rest < 0){
            result = i + 1;
            rest = 0;
        }
    }
    if(total < 0){
        return -1;
    }
    return result;
}

分发糖果

. - 力扣(LeetCode)

该题要保证每个小孩和左右比较,除两端孩子。定义一个candy数组,保存每个孩子的糖果数。一次遍历无法比较两边孩子。

上代码

#define max(a, b) (((a) > (b)) ? (a) : (b))
int candy(int* ratings, int ratingsSize) {
    int* candy = (int*)malloc(sizeof(int) * ratingsSize);
    for(int i = 0; i < ratingsSize; i++){
        candy[i] = 1;
    }
    for(int i = 1; i < ratingsSize; i++){
        if(ratings[i] > ratings[i - 1]){
            candy[i] = candy[i - 1] + 1;
        }
    }
    for(int i = ratingsSize - 2; i >=0 ; i--){
        if(ratings[i] > ratings[i + 1]){
            candy[i] = max(candy[i], candy[i + 1] + 1);
        }
    }
    int result = 0;
    for(int i = 0; i < ratingsSize; i++){
        result += candy[i];
    }
    free(candy);
    return result;
}

由于一次遍历无法比较左右孩子,所以进行两次,每个孩子的糖果应该为两次遍历最大的糖果数,这样就可以满足两边的大小。

柠檬水找零

. - 力扣(LeetCode)

顾客只给三种金额,5,10,20。柠檬水每杯5块,那么遇到5块不需找零,10块找零5块。这两种情况是固定的。遇到20的时候就有两种找零情况,一张10块,一张5块,或者3张5块。这里给一张10块和一张5块是局部最优,贪心的点也在这。

放码过来

bool lemonadeChange(int* bills, int billsSize) {
  int five = 0, ten = 0, twenty = 0;
  for(int i = 0; i < billsSize; i++){
    if(bills[i] == 5){
        five++;
    }
    else if(bills[i] == 10){
        if(five > 0){
            ten++;
            five--;
        }
        else{
            return false;
        }
    }
    else{
        if(ten > 0 && five > 0){
            ten--;
            five--;
        }
        else if(five >= 3){
            five -= 3;
        }
        else{
            return false;
        }
    }
  }  
  return true;
}

根据身高重建队列

. - 力扣(LeetCode)

这题贪心的点并不好找,题意大体是,给定一个二维数组,一维数组是两个整数,前一个表示身高,后一个表示这个人前面有几个比它高或等于和它身高的人数。每个一维的整数是h,k。

思路是:先将这个给定的二维数组排序,根据身高从高到矮排,若身高相等,则根据前面的人数少到多排。然后根据k取插入排序。

原因:1、为什么要身高从高到矮排列?因为排列完之后是根据k的值来插入,矮的后插并不会影响前面高个子的k。2、当身高相同时,为什么要根据k的值从小到大排。插入时,k值小的先插,大的后插,这样k值大的一定插在k值小的后面,符合题意。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        vector<vector<int>> que;
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1];
            que.insert(que.begin() + position, people[i]);
        }
        return que;
    }
};

用最少数量的箭射爆气球

. - 力扣(LeetCode)

题意分析:给你一个二维数组,这个数组的每一个一维表示一个气球的区间,(这个气球的直径等于这个区间的差)。假如有两个气球是有一部分重合的,那么一根箭就能射爆两个气球。题目要求最少的箭射爆全部的气球。

思路:这题目就是要求重合的区间。上图可能会更好理解。

上码

class Solution {
protected:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0] < b[0]; //注意这里只能比较,不能相减,否则值溢出。
    }
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), cmp);
        int res = 1;
        for(int i = 1; i < points.size(); i++){
            if(points[i][0] > points[i - 1][1]){
                res++;
            }
            else{
                points[i][1] = min(points[i][1], points[i - 1][1]);
            }
        }
        return res;
    }
};

res表示的箭的数量,这里起始值是1,表示至少需要一根箭。

下一个区间的左端点和上一个区间的右端点比较,假如小于或等于,表示这两个区间有重合的部分,然后更新这个区间的右端点,要更新为和上一个区间的右端点的最小值。

无重叠区间

. - 力扣(LeetCode)

这一题和上一题思路差不多。其实题意就是求重叠区间的个数。

思路:先排序,那么这里有两个选择,是按照左边界排列,还是右边界呢?其实这两种都可以,这里我是排列左边界。排列左边界时,假如两个区间的左边界相同,那么该怎么排呢,其实根本不用管右端点,我们直接在循环里面处理它就行了。而处理的过程就涉及到了一个贪心的点,怎样才能删除最少的区间呢?这里当我们判断到一个区间的左端点小于上一个区间的右端点,那么肯定这两个区间是重叠的,我们这里需要把当前区间的右端点更新为两个区间右端点的最小值,这样我们这个区间距离下一个区间就会跟远,自然删除的区间也就最小。而这样处理也就完美包含了当两个区间左端点相同时,右端点哪个大?该怎么排?

还有一种情况,画图说明吧。更好理解。

上码

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0] < b[0];
    }
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int res = 0;
        for(int i = 1; i < intervals.size(); i++){
            if(intervals[i][0] < intervals[i - 1][1]){
                res++;
                intervals[i][1] = min(intervals[i][1], intervals[i - 1][1]);
            }
        }
        return res;
    }
};

划分内容字母区间

. - 力扣(LeetCode)

题意:给定一个字符串,要求分成尽可能多的子串,每个子串的字母不在其他子串中出现。

思路:要尽可能多的子串,那么就从字符串第一个字符开始,找到这个字符最远出现的位置,这个位置到第一个字符就是一个子串。那么问题是如何找这个字符最远出现的位置呢?这里就可以使用哈希表,哈希表的下标是每个字母,存储的元素是该字母的最远位置。然后遍历整个字符串。

上代码:

int max(int a, int b){
    return a > b ? a : b;
}
int* partitionLabels(char* s, int* returnSize) {
    int hash_table[27] = {0};
    for(int i = 0; i < strlen(s); i++){
        hash_table[s[i] - 'a'] = i;
    }
    int left = 0, right = 0;
    int* ret = malloc(sizeof(int) * strlen(s));
    *returnSize = 0;
    for(int i = 0; i < strlen(s); i++){
        right = max(right, hash_table[s[i] - 'a']);
        if(i == right){
            ret[(*returnSize)++] = right - left + 1;
            left = i + 1;
        }
        
    }
    return ret;
}

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
贪心算法是一种基于贪心思想的算法,它在每一步选择中都采取当前状态下最优的选择,从而希望最终得到全局最优解。贪心算法通常可以用来解决一些最优化问题,比如最小生成树、背包问题、最短路径等。 贪心算法的实现步骤一般如下: 1. 定义问题的贪心策略。 2. 根据贪心策略,选择当前状态下的最优解。 3. 更新问题的状态,继续步骤 2 直到达到终止条件。 需要注意的是,贪心算法并不是所有问题都能使用的算法。对于某些问题,贪心算法可能得到的不是全局最优解,而是局部最优解。因此,在使用贪心算法时,需要保证问题具有贪心选择性质和无后效性质。 下面以一个简单的例子来说明贪心算法的应用。 给定一个数组,每个元素表示一个活动的结束时间和开始时间。你作为一个主办人,需要在这些活动中选择尽可能多的活动进行安排,使得不同的活动之间不会产生时间冲突。求最多能安排多少个活动。 示例代码: ```csharp public class Activity { public int start; public int end; public Activity(int start, int end) { this.start = start; this.end = end; } } public int MaxActivities(Activity[] activities) { int count = 0; int currentEnd = 0; Array.Sort(activities, (a, b) => a.end - b.end); foreach (Activity activity in activities) { if (activity.start >= currentEnd) { count++; currentEnd = activity.end; } } return count; } ``` 在这个示例代码中,我们定义了一个 `Activity` 类来表示活动的开始时间和结束时间。然后,我们通过贪心策略来选择活动,即每次选择结束时间最早的活动。具体实现中,我们将活动按照结束时间从小到大排序,然后依次选择活动,如果当前活动的开始时间大于等于前一个活动的结束时间,则可以选择该活动。最后返回选择的活动数即可。 时间复杂度为 O(n log n),其中 n 是活动数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值