代码随想录(八):贪心算法

455.分发饼干

题目链接

C++代码:

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int res = 0;
        for (int i = 0, j = 0; i < g.size(); i++) {
            while (j < s.size() && s[j] < g[i]) j++;
            if (j < s.size()) {
                res++;
                j++;
            }
        }
        return res;
    }
};

376. 摆动序列

题目链接

解题思路
第 1 步:摆动序列的连续数字之间的差需要在正数和负数之间交替,因此首先需要去除连续相同的数
第 2 步:连续数字的收尾值一定在摆动序列中,同时找出数组中的局部最大值和局部最小值放入摆动序列中。请添加图片描述
C++代码:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        nums.erase(unique(nums.begin(), nums.end()), nums.end());
        int res = 2;
        if (nums.size() <= 2) return nums.size();
        for (int i = 1; i < nums.size() - 1; i++) {
            if (nums[i] > nums[i - 1] && nums[i] > nums[i + 1]) res++;
            if (nums[i] < nums[i - 1] && nums[i] < nums[i + 1]) res++;
        }
        return res;
    }
};

53. 最大子数组和

题目链接

解题思路

局部最优:当前连续和为负数的时候立刻放弃,从下一个元素重新计算连续和,因为负数加上下一个元素连续和只会越来越小。
全局最优:选取最大连续和
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。

从代码角度上来讲:
遍历 nums,从头开始用 sum 累积,如果 sum 一旦加上 nums[i] 变为负数,那么就应该从 nums[i+1] 开始从 0 累积 sum 了,因为已经变为负数的 sum,只会拖累总和。

这相当于是暴力解法中的不断调整最大子序和区间的起始位置。

C++代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum = 0, res = INT_MIN;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
            if (sum > res) res = sum; // 取区间累计的最大值(相当于不断确定最大子序终止位置)
            if (sum < 0) sum = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
        }
        return res;
    }
};

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

题目链接

解题思路

题意是获得利润的最大值,可把这个问题看作:当天买入股票,后一天卖出。若后一天卖出时出现亏损,则头一天也就不买入。这样在算利润时,只把每天买卖股票收益为正值时,计入最终总利润。

C++代码:

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

55. 跳跃游戏

题目链接

解题思路

题意:数组中的每个元素代表你在该位置可以跳跃的最大长度,判断最终是否能够到达最后一个下标。
使用head来记录能够到达的最远的位置,如果当前位置能够到达的最远位置比head要远,则更新head。通过head可判断最终能否到达最后一个下标。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围head
在这里插入图片描述

C++代码:

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int head = 0;
        if (nums.size() == 1) return true; // 只有一个元素,就是能达到
        for (int i = 0; i <= head; i++) { // 注意这里是小于等于head
            head = max(head, i + nums[i]);
            if (head >= nums.size() - 1) return true; // 说明可以到达到终点了
        }
        return false;
    }
};

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

题目链接

解题思路
题意:对数组中的元素进行k次取反,使得最终数组中的元素和最大。
首先将元素从小到大进行排序,依次让数组中较小的负数变为正数,可使元素和达到最大。
如果将所有的负数都转变为正数了,而k依然大于0,此时判断k是否为奇数,若k为奇数,则从小将数组进行排序,将排序后的最小非负数转变为负值。最后求得数组中元素和即可。

C++代码:

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int res = 0;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] < 0 && k > 0) { // 依次将较小的负数转变为正数
                nums[i] *= -1;
                k--;
            }
        }
        if (k % 2 == 1) { // 此时将最小的非负数乘上-1
            sort(nums.begin(), nums.end());
            nums[0] *= -1;
        }
        for (auto c: nums) res += c;
        return res;
    }
};

134. 加油站

题目链接

解题思路
由于环路上有 n 个加油站,那么我们可以从 n 个加油站中依次选取第 i 个加油站作为起点进行讨论
如果把第 i 个加油站作为起点,到达第 i + j 个加油站时,发现当前汽车剩余油量cur_gas与第i + j个加油站的汽油总和小于 cost[i + j] 时,也就意味着此时汽车无法到达第i + j + 1个加油站,那么可推出第 i 个加油站到第 i + j 个加油站中任意一个加油站作为起点都无法到达第i + j + 1 个加油站,因此接下来将第i + j + 1个加油站作为起点继续讨论。

C++代码:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size(); // 加油站的个数
        for (int i = 0, j; i < n; ) {
            int cur_gas = 0; // 处于起点的汽车剩余油量为0
            for (j = 0; j < n; j++) { // 从第i个加油站出发,观察汽车能够走到哪个加油站
                int k = (i + j) % n; 
                cur_gas += gas[k]; // 当汽车到达第 k 个加油站时,加上gas[k]的汽油量
                if (cur_gas - cost[k] < 0) break; // 当前汽车的油量无法到达下一个加油站
            }
            if (j == n) return i; // 表明此时汽车可以绕环行驶一周
            i = i + j + 1; // 此时表明第i个加油站到第i+j个加油站作为起点都无法到达第i+j+1个加油站
                           // 因此把第i+j+1个加油站作为起点继续讨论 
        }
        return -1;
    }
};

860. 柠檬水找零

题目链接

解题思路
fiveten两个变量来存储钱包里的5美元和10美元分别有多少张,接着分情况讨论:

  • 如果顾客支付的是5美元,则five++
  • 如果顾客支付的是10美元,则先要判断当前钱包中是否有5美元的纸币,然后在进行找零
  • 如果顾客支付的是20美元,则首先判断当前钱包中是否同时拥有一张10美元和一张5美元,或者钱包中包含三张5美元,只有这两种情况才能进行找零。

C++代码:

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int five = 0, ten = 0;
        for (auto x: bills) {
            if (x == 5) five++;
            else if (x == 10) {
                if (!five) return false;
                five--;
                ten++;
            }else {
                if (ten && five) ten--, five--;
                else if (five >= 3) five -= 3;
                else return false;
            }
        }
        return true;
    }
};

135. 分发糖果

题目链接

解题思路
该题采用贪心的策略,重点是一定要先确定一边之后,再确定另一边,如果在考虑局部的时候想两边兼顾,就会顾此失彼。
该题采用两次贪心的策略:

  • 首先从左向右遍历,比较右边孩子评分比左边高的情况,使得右边孩子的糖果比相邻的左边孩子糖果数多一。
  • 然后从右向左遍历,比较左边孩子评分比右边高的情况,那么如果左边孩子的评分高,在确定左边孩子糖果数时,需要将当前左边孩子的糖果数与右边孩子糖果数+1进行比较,取两者的最大值作为左边孩子的糖果数,这样就能满足相邻两个孩子评分更高的孩子会获得更多的糖果。

C++代码:

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> nums(ratings.size(), 1);
        // 从前向后
        for (int i = 1; i < nums.size(); i++) {
            if (ratings[i] > ratings[i - 1]) nums[i] = nums[i - 1] + 1;
        }
        // 从后向前
        for (int i = nums.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) nums[i] = max(nums[i], nums[i + 1] + 1);
        }
        // 统计结果
        int res = 0;
        for (auto c: nums) res += c;
        return res;
    }
};

406. 根据身高重建队列

题目链接

解题思路
这道题与上一题分发糖果有一点点像,都涉及到两个维度,其技巧就是先确定一边然后贪心另一边,两边一起考虑,就会顾此失彼。
首先我们可以将序列按照身高h从大到小来排序,身高相同的话则k小的站前面,让高个子在前面。
这样做的目的主要是为了后边可以按照第二个维度k来将元素逐个重新插入到结果队列中。
优先按身高高的people的k来插入结果队列,后续插入的节点也不会影响前面已经插入的节点,最终按照k的规则完成了结果对列。

整个插入过程如下:

排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]

插入的过程:

  • 插入[7,0]:[[7,0]]
  • 插入[7,1]:[[7,0],[7,1]]
  • 插入[6,1]:[[7,0],[6,1],[7,1]]
  • 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
  • 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
  • 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

此时就按照题目的要求完成了重新排列。

C++代码:

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    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);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};

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

题目链接

解题思路
该题为区间选点的问题
首先将区间按照右端点进行排序,然后将弓箭的位置初始化为第一个区间的右端点。
从前往后遍历所有区间,如果当前区间与弓箭所处位置不想交,这时就更新弓箭,将弓箭的位置重置为当前区间的右端点,并把弓箭数+1。

C++代码:

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 res = 1, ed = points[0][1]; // 弓箭的位置ed初始化为第一个气球的右端点
        for (int i = 1; i < points.size(); i++) {
            if (points[i][0] > ed) { // 新增一支弓箭,并把位置初始化为当前气球的右端点处
                res++;
                ed = points[i][1];
            }
        }
        return res;
    }
};

435. 无重叠区间

题目链接

解题思路
本题的题意是从给定的区间集合中移除区间,使得剩余区间互不重叠,求需要移除区间的最小数量。
可以先求该问题的对偶问题:在一个区间集合中选择若干个区间,使得选中的区间之间互不重叠,求可选取区间的最大数量 。

C++代码:

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

    int eraseOverlapIntervals(vector<vector<int>>& q) {
        sort(q.begin(), q.end(), cmp);

        int res = 1, ed = q[0][1];
        for (int i = 1; i < q.size(); ++i) {
            if (ed <= q[i][0]) {
                res++;
                ed = q[i][1];
            }
        }
        return q.size() - res;
    }
};

763. 划分字母区间

题目链接

解题思路
首先需要记录下每个字母最后一次出现的位置,然后从前往后遍历整个数组,不断更新片段的末尾位置,当走到片段末尾时,将当前片段放入结果中,更新片段不断循环下去。

C++代码:

class Solution {
public:
    vector<int> partitionLabels(string s) {
        unordered_map<int, int> last;
        for (int i = 0; i < s.size(); i++) last[s[i]] = i;
        vector<int> res;
        int start = 0, end = 0;
        for (int i = 0; i < s.size(); i++) {
            end = max(end, last[s[i]]);
            if (i == end) {
                res.push_back(end - start + 1);
                start = end = i + 1;
            }
        }
        return res;
    }
};

56. 合并区间

题目链接

解题思路
首先将区间按照左端点从小到大进行排序,然后将第一个区间作为需要合并的维护区间,依次判断后续的区间是否和维护区间存在交集。
如果当前的区间与维护区间相交,则更新维护区间的右端点;如果当前区间与维护区间不相交,则两个区间不能合并,把维护区间保存到结果数组中,并将维护区间更新为当前区间,继续进行循环判断。

C++代码:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& q) {
        sort(q.begin(), q.end());

        int st = q[0][0], ed = q[0][1];
        vector<vector<int>> res;
        for (int i = 1; i < q.size(); i++) {
            if (q[i][0] <= ed) ed = max(ed, q[i][1]);
            else {
                res.push_back({st, ed});
                st = q[i][0];
                ed = q[i][1];
            }
        }
        res.push_back({st, ed});

        return res;
    }
};

738. 单调递增的数字

题目链接

解题思路
本题需要找到小于或等于n的最大数字,且数字呈单调递增。也就是说如果数字出现了单调递减,我们需要找到数字从高位到低位查找首次出现单调递减的位置k,并且判断位置k前面的数字str[k - 1]是否等于第k位的数字str[k],找到等于str[k]的最高位,将该位的值减1,并将其后边的低位全部置为9,这样才能保障数字单调递增。
请添加图片描述

C++代码:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& q) {
        sort(q.begin(), q.end());

        int st = q[0][0], ed = q[0][1];
        vector<vector<int>> res;
        for (int i = 1; i < q.size(); i++) {
            if (q[i][0] <= ed) ed = max(ed, q[i][1]);
            else {
                res.push_back({st, ed});
                st = q[i][0];
                ed = q[i][1];
            }
        }
        res.push_back({st, ed});

        return res;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员小浩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值