LeetCode【代码随想录】刷题(贪心算法篇)

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 res = 0;
        int i = 0, j = 0;
        while(i < g.size() && j < s.size())
        {
            if(s[j] >= g[i])
            {
                i++;
                j++;
                res++;
            }
            else
                j++;
        }
        return res;
    }
};

376. 摆动序列

力扣题目链接

题目:如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为**摆动序列 **。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组nums ,返回nums 中作为摆动序列的最长子序列的长度 。

思路:贪心,将差值的正负交替看成序列峰谷起伏。

容易看出要想得到最长子序列要删除的就是峰谷之间的过渡元素,最长子序列的长度就是峰谷个数+1。由于只需要长度,所以统计峰谷个数即可。用prediff表示前一个符合条件的差值,curdiff表示当前差值。峰谷交替即可表示为:prediff <= 0 && curdiff > 0 || prediff >= 0 && curdiff < 0。等于0是为了处理开头两个元素相等的情况。

通过代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size() < 2)
            return nums.size();
        int prediff = nums[1] - nums[0];
        int res = (prediff == 0 ? 1 : 2);
        for(int i = 2; i < nums.size(); i++)
        {
            int curdiff = nums[i] - nums[i - 1];
            if(prediff <= 0 && curdiff > 0 || prediff >= 0 && curdiff < 0)
            {
                res++;
                prediff = curdiff;
            }
        }
        return res;
    }
};

53. 最大子序和

力扣题目链接

题目:给你一个整数数组nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

思路:这道题经典做法应该是动态规划,但是贪心也能做。当我们遍历数组统计和的时候,如果某个值加进去之后,使得总和变成负数了,说明这个值不应该算进去(这一段子序列到头了),应该从下一个开始重新计算总和。为什么是负数,因为但凡数组里有一个正数,最大和至少可以单取这一个正数。如果都是负数,最大和明显就是单取那个最大负数。

通过代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN;
        int cnt = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            cnt += nums[i];
            res = max(res, cnt);
            if(cnt < 0)
                cnt = 0;
        }
        return res;
    }
};

122.买卖股票的最佳时机 II

力扣题目链接

题目:给你一个整数数组prices ,其中prices[i]表示某支股票第i天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回你能获得的最大利润 。

思路:把利润分解为每天为单位的维度,然后收集每天的正利润。

通过代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int res = 0;
        for(int i = 1; i < prices.size(); i++)
        {
            int diff = prices[i] - prices[i - 1];
            res += max(diff, 0);
        }
        return res;
    }
};

55. 跳跃游戏

力扣题目链接

题目:给你一个非负整数数组nums,你最初位于数组的第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回true ;否则,返回false

思路:不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。这个范围内,别管是怎么跳的,反正一定可以跳过来。那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

通过代码:

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        for(int i = 0; i <= cover; i++)
        {
            cover = max(cover, i + nums[i]);
            if(cover >= nums.size() - 1)
                return true;
        }
        return false;
    }
};

45.跳跃游戏 II

力扣题目链接

题目:给定一个长度为n的0索引整数数组nums。初始位置为nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达nums[n - 1]

思路:考虑两个范围,一个是当前范围,一个是下一步范围。当前范围是已经计算过的以某一最少步数可到达的范围,并且当前范围覆盖不到终点,例如:以最少步数0可到达的范围为nums[0]。通过遍历当前范围,即比较i + nums[i],可以得到下一步的最大范围,下一步范围的最少步数即为当前步数+1。接上例:下一步范围为0 + nums[0],该范围内的最少步数都为0 + 1。当遍历到当前范围的边界时,也正好得到下一步最大范围。由于当前范围覆盖不到终点,所以需要继续迭代,此时需要步数+1,并把下一步范围当作新的当前范围。若下一步范围(也就是新的当前范围)能够覆盖终点,即可退出遍历。

通过代码:

class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size() == 1)
            return 0;
        int ans = 0, cur_cover = 0, next_cover = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            next_cover = max(next_cover, i + nums[i]);
            if(i == cur_cover)
            {
                ans++;
                cur_cover = next_cover;
                if(next_cover >= nums.size() - 1)
                    break;
            }
        }
        return ans;
    }
};

1005.K次取反后最大化的数组和

力扣题目链接

题目:给你一个整数数组nums和一个整数k,按以下方法修改该数组:

  • 选择某个下标i并将nums[i]替换为-nums[i]

重复这个过程恰好k次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和 。

思路:肯定优先翻转负数,并且是最小的负数。如果负数反转完了,那就翻转最小的正数。

通过代码:

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int res = 0, min_index = 0;
        sort(nums.begin(), nums.end());
        for(int i = 0; i < nums.size(); i++)
        {
            if(nums[i] < 0 && k > 0)
            {
                k--;
                nums[i] = -nums[i];
            }
            res += nums[i];
            if(nums[i] < nums[min_index])
                min_index = i;
        }
        if(k % 2 == 0)
            return res;
        return res - 2 * nums[min_index];
    }
};

134. 加油站

力扣题目链接

题目:在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

思路:每个加油站的剩余量rest[i]gas[i] - cost[i]。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

通过代码:

class Solution {
public:

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int start = 0, total = 0, rest = 0;
        for(int i = 0; i < gas.size(); i++)
        {
            total += gas[i] - cost[i];
            rest += gas[i] - cost[i];
            if(rest < 0)
            {
                start = i + 1;
                rest = 0;
            }
        }
        if(total < 0)
            return -1;
        return start;
    }
};

135. 分发糖果

力扣题目链接

题目:n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

思路:如果将评分大小看成上升下降的波峰和波谷的话(参考376.摆动序列的图),很明显最佳策略是从波谷向两边的坡上扩展,糖果数依次+1。因此,我们只需按照坡度上升的方向处理即可,先正向遍历一遍,再反向遍历一遍。反向遍历是为了将下降的坡转化为上升坡来处理。

通过代码:

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        vector<int> candies(n, 1);
        for(int i = 1; i < n; i++)
        {
            if(ratings[i] > ratings[i - 1])
                candies[i] = candies[i - 1] + 1;
        }
        for(int i = n - 1; i > 0; i--)
        {
            if(ratings[i - 1] > ratings[i])
                candies[i - 1] = max(candies[i - 1], candies[i] + 1);
        }
        int res = 0;
        for(int num : candies)
            res += num;
        return res;
    }
};

860.柠檬水找零

力扣题目链接

题目:在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

思路:模拟即可,注意给20找零的时候优先给10块的。

通过代码:

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int cash[2] = {0, 0};
        for(int i = 0; i < bills.size(); i++)
        {
            if(bills[i] == 5)
                cash[0]++;
            else if(bills[i] == 10)
            {
                cash[1]++;
                cash[0]--;
            }
            else
            {
                if(cash[1] > 0)
                {
                    cash[1]--;
                    cash[0]--;
                }
                else
                    cash[0] -=3;
            }
            if(cash[0] < 0)
                return false;
        }
        return true;
    }
};

406.根据身高重建队列

力扣题目链接

题目:假设有打乱顺序的一群人站成一个队列,数组people表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki]表示第i个人的身高为 hi ,前面正好有ki个身高大于或等于hi的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

思路:由于高个子的位置会影响矮个子,所以应该先确定高个子。对people数组排序,高个子在前,矮个子在后,一样高的按ki升序。排完序之后,每个人前面都是比他高的,所以下标i就能表示前面有i个人比他高。所以,为了让k回到正确的位置,只需要按照k为下标重新插入队列就可以了(这时前面正好有k个比他高的)。另外,为了减少插入移动开销,采用链表存储。

通过代码:

class Solution {
public:
    static bool cmp(vector<int> &a, 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);
        list<vector<int>> li;
        for(int i = 0; i < people.size(); i++)
        {
            int pos = people[i][1];
            auto iter = li.begin();
            advance(iter, pos);
            li.insert(iter, people[i]);
        }
        return vector<vector<int>>(li.begin(), li.end());
    }
};

452. 用最少数量的箭引爆气球

力扣题目链接

题目:有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组points ,其中points[i] = [x_start, x_end] 表示水平直径在x_startx_end之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为x_startx_end, 且满足x_start ≤ x ≤ x_end,则该气球会被引爆 。可以射出的弓箭的数量没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points ,返回引爆所有气球所必须射出的最小弓箭数 。

思路:很明显优先射重叠的气球,并且重叠要尽可能多。所以先对气球排序,排序之后遍历气球,并维护一个重叠气球的最小右边界。在最小右边界之前肯定是要射一箭的。当遍历到某个气球的左边界大于最小右边界,说明这个气球不和前面的气球重叠,这时射出这一箭,同时要开始维护新的重叠气球右边界。

通过代码:

class Solution {
public:
    static bool cmp(vector<int> &a, vector<int> &b){
        return a[0] < b[0];
    }

    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), cmp);
        int res = 0, min_right = INT_MAX;
        for(int i = 0; i < points.size(); i++)
        {
            if(points[i][0] <= min_right)
                min_right = min(min_right, points[i][1]);
            else
            {
                res++;
                min_right = points[i][1];
            }
        }
        return res + 1;
    }
};

435. 无重叠区间

力扣题目链接

题目:给定一个区间的集合intervals ,其中intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

思路:如果能求出无重叠区间的数量,再用总数去剪就是需要移除区间的数量。求无重叠区间类似上一题。因为上一题每遇到一个不重叠的区间就需要对之前重叠的射出一箭。

通过代码:

class Solution {
public:
    static bool cmp(vector<int> &a, vector<int> &b){
        if(a[0] == b[0])
            return a[1] < b[1];
        return a[0] < b[0];
    }

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int cnt = 1, min_right = INT_MAX;
        for(int i = 0; i < intervals.size(); i++)
        {
            if(intervals[i][0] < min_right)
                min_right = min(min_right, intervals[i][1]);
            else
            {
                cnt++;
                min_right = intervals[i][1];
            }
        }
        return intervals.size() - cnt;
    }
};

763.划分字母区间

力扣题目链接

题目:给你一个字符串s。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s

返回一个表示每个字符串片段的长度的列表。

思路:由于同一个字母只能出现在一个片段里,所以需要找该字母出现的最远距离,也就是string的find_last_of函数。在这个片段里,还有其他字母,其他字母可能有更远的距离,所以遍历一下找到最远的即可。再用一个unordered_map避免重复操作。

通过代码:

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int> res;
        int start = 0;
        while(start < s.size())
        {
            unordered_set<char> hash;
            int end = start;
            for(int i = start; i < end + 1; i++)
            {
                if(hash.find(s[i]) != hash.end())
                    continue;
                hash.insert(s[i]);
                int pos = s.find_last_of(s[i]);
                end = max(end, pos);
            }
            res.push_back(end - start + 1);
            start = end + 1;
        }
        return res;
    }
};

56. 合并区间

力扣题目链接

题目:以数组intervals表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

思路:先排序,让所有的相邻区间尽可能的重叠在一起。然后依次遍历,若重叠就合并。

通过代码:

class Solution {
public:
    static bool cmp(vector<int> &a, vector<int> &b){
        return a[0] < b[0];
    }

    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if(intervals.size() == 0)
            return {};
        vector<vector<int>> res;
        sort(intervals.begin(), intervals.end(), cmp);
        int left = intervals[0][0], max_right = intervals[0][1];
        for(int i = 1; i < intervals.size(); i++)
        {
            if(intervals[i][0] <= max_right)
                max_right = max(max_right, intervals[i][1]);
            else
            {
                res.push_back({left, max_right});
                left = intervals[i][0];
                max_right = intervals[i][1];
            }
        }
        res.push_back({left, max_right});
        return res;
    }
};

738.单调递增的数字

力扣题目链接

题目:当且仅当每个相邻位数上的数字xy满足x <= y 时,我们称这个整数是单调递增的。

给定一个整数n ,返回小于或等于n的最大数字,且数字呈单调递增 。

思路:从左往右遍历,找到第一个下降的数字的位置,将这个数字减一,其后数位都变成9。如果这个数字前面还有连续的一样的数字,得找到最左边的。例如332,第一个下降的数字为3,但3前面还有相邻的一样的3,所以得找到最左边的3。将3减一,其后数位变成9,得到299。

通过代码:

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string num = to_string(n);
        int pos = num.size();
        for(int i = 0; i < num.size() - 1; i++)
        {
            if(num[i] > num[i + 1])
            {
                pos = i;
                break;
            }
        }
        if(pos != num.size())
        {
            while(pos > 0 && num[pos - 1] == num[pos])
                pos--;
            num[pos]--;
            for(int i = pos + 1; i < num.size(); i++)
                num[i] = '9';
        }
        return stoi(num);
    }
};

968.监控二叉树

力扣题目链接

题目:给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

思路:头节点不放摄像头只能省一个,而叶节点不放摄像头省下的是指数级的。所以应该从下往上来安排摄像头,即采用后续遍历。而且,最好隔一个放摄像头,避免覆盖范围重合。用0表示本节点无覆盖,1表示本节点有摄像头,2表示本节点有覆盖。单次递归根据左右孩子的状态来决定本节点的返回状态。如果左右孩子但凡有一个没有覆盖,那么本节点就必须要放了;如果左右孩子都有覆盖了,并且其中有一个还有摄像头,那么本节点已经被覆盖到了;如果左右孩子都只是有覆盖,那么为了隔一个安放,本节点不安排摄像头,返回无覆盖状态。

通过代码:

class Solution {
public:
    int res = 0;

    int traverse(TreeNode *root){
        //0: 本节点无覆盖
        //1: 本节点有摄像头
        //2: 本节点有覆盖
        if(!root)
            return 2;
        int left = traverse(root -> left);
        int right = traverse(root -> right);
        if(left == 0 || right == 0)
        {
            res++;
            return 1;
        }
        if(left == 1 || right == 1)
            return 2;
        return 0;
    }

    int minCameraCover(TreeNode* root) {
        int r = traverse(root);
        if(r == 0)
            res++;
        return res;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

h0l10w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值