算法【贪心经典题目专题1】

狭义的贪心指每一步都做出在当前状态下最好或最优的选择,从而希望最终的结果是最好或最优的算法。

广义的贪心指通过分析题目自身的特点和性质,只要发现让求解答案的过程得到加速的结论,都算广义的贪心。

贪心是最符合自然智慧的思想,一般分析门槛不高。理解基本的排序、有序结构,有基本的逻辑思维就能理解。但是贪心的题目,千题千面,极难把握。难度在于证明局部最优可以得到全局最优。

有关贪心的若干现实 & 提醒

1)不要去纠结严格证明,每个题都去追求严格证明,浪费时间、收益很低,而且千题千面。玄学

2)一定要掌握用对数器验证的技巧,这是解决贪心问题的关键

3)解法几乎只包含贪心思路的题目,代码量都不大

4)大量累积贪心的经验,重点不是证明,而是题目的特征,以及贪心方式的特征,做好总结方便借鉴

5)关注题目数据量,题目的解可能来自贪心,也很可能不是,如果数据量允许,能不用贪心就不用(稳)

6)广义的贪心无所不在,可能和别的思路结合,一般都可以通过自然智慧想明白,依然不纠结证明

下面通过题目加深理解。

题目一

测试链接:179. 最大数 - 力扣(LeetCode)

分析:这道题很容易看出来,需要进行一个排序,然后遍历排序后的数组即可得到答案,但排序采用的比较方法不应该在每一位上去比较,而是直接将两个数字组合起来进行比较。代码如下。

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        vector<string> num;
        for(int i = 0;i < nums.size();++i){
            num.push_back(to_string(nums[i]));
        }
        sort(num.begin(), num.end(), [](string& s1, string& s2){
            return s1 + s2 > s2 + s1;
        });
        if(num[0] == "0"){
            return "0";
        }
        string ans = "";
        for(int i = 0;i < num.size();++i){
            ans += num[i];
        }
        return ans;
    }
};

题目二

测试链接:1029. 两地调度 - 力扣(LeetCode)

分析:这道题先假设所有人都去a地,然后进行一个排序即去b地比去a地的代价小的排前面。然后选前n个人去b地,后n个人去a地即可得到答案。代码如下。

class Solution {
public:
    int twoCitySchedCost(vector<vector<int>>& costs) {
        int length = costs.size();
        int sum = 0;
        sort(costs.begin(), costs.end(), [](vector<int>& v1, vector<int>& v2){
            return (v1[1] - v1[0]) < (v2[1] - v2[0]);
        });
        for(int i = 0;i < length/2;++i){
            sum += costs[i][1];
        }
        for(int i = length/2;i < length;++i){
            sum += costs[i][0];
        }
        return sum;
    }
};

题目三

测试链接:1553. 吃掉 N 个橘子的最少天数 - 力扣(LeetCode)

分析:首先能按比例吃肯定选择按比例吃,所以吃掉一个橘子可以理解为按比例吃服务即吃掉一个橘子是为了形成2或3的倍数。然后进行可能性的展开即一个一个吃之后吃掉n/2个或者一个一个吃之后吃掉2n/3个取最大值。代码如下。

class Solution {
public:
    map<int, int> dp;
    int minDays(int n) {
        if(n <= 1){
            return n;
        }
        map<int, int>::iterator pos = dp.find(n);
        if(pos != dp.end()){
            return (*pos).second;
        }
        int value = min(n % 2 + 1 + minDays(n / 2), n % 3 + 1 + minDays(n/3));
        dp.insert(pair<int, int>(n, value));
        return value;
    }
};

题目四

测试链接:线段重合_牛客题霸_牛客网

分析:注意到一个线段重合的区域一定是以某一条线段的起点为左边界,所以首先按起点从小到大进行排序,并且用一个小根堆维护线段的终点。对于遍历到的线段,先清除小根堆中线段的终点小于等于当前线段起点的终点,然后将当前线段的终点加入小根堆,现在小跟堆中终点的个数就是以当前线段起点为左边界的线段重合最大数,遍历所有线段即可得到答案。代码如下。

#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
int N;
int main(void){
    scanf("%d", &N);
    vector<pair<int, int>> line(N);
    for(int i = 0;i < N;++i){
        scanf("%d%d", &line[i].first, &line[i].second);
    }
    sort(line.begin(), line.end(), [](pair<int, int>& a, pair<int, int>& b){
        return a.first < b.first;
    });
    auto cmp = [](int& a, int& b){
        return a > b;
    };
    priority_queue<int, vector<int>, decltype(cmp)> p(cmp);
    int ans = 0;
    for(int i = 0;i < N;++i){
        while (!p.empty() && p.top() <= line[i].first)
        {
            p.pop();
        }
        p.push(line[i].second);
        ans = max(ans, (int)p.size());
    }
    printf("%d", ans);
    return 0;
}

题目五

测试链接:630. 课程表 III - 力扣(LeetCode)

分析:首先按结束时间从小到大进行排序,然后进行遍历。如果当前时间加上持续时间小于等于结束时间,则可以直接修读;如果当前时间加上持续时间大于结束时间,则需要对已经修读的课程中持续时间最大的课程进行比较,如果当前课程的持续时间小于已经修读的课程中最大的持续时间则修读当前课程,不修读最大持续时间的课程。代码如下。

class Solution {
public:
    int scheduleCourse(vector<vector<int>>& courses) {
        sort(courses.begin(), courses.end(), [](vector<int>& v1, vector<int>& v2){
            return (v1[1] < v2[1]) || (v1[1] == v2[1] && v1[0] < v2[0]);
        });
        auto cmp = [](int& a, int& b){
            return a < b;
        };
        priority_queue<int, vector<int>, decltype(cmp)> p(cmp);
        int time = 0;
        int temp;
        for(int i = 0;i < courses.size();++i){
            if(time + courses[i][0] <= courses[i][1]){
                time += courses[i][0];
                p.push(courses[i][0]);
            }else{
                if(!p.empty() && courses[i][0] < p.top()){
                    time -= p.top();
                    time += courses[i][0];
                    p.pop();
                    p.push(courses[i][0]);
                }
            }
        }
        return p.size();
    }
};

题目六

测试链接:Welcome - Luogu Spilopelia

分析:这道题是明确知道需要合并n-1次,所以每一次合并都选取数目最小的两个堆进行合并。代码如下。

#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int n;
int main(void){
    scanf("%d", &n);
    auto cmp = [](int& a, int& b){
        return a > b;
    };
    priority_queue<int, vector<int>, decltype(cmp)> p(cmp);
    int temp;
    for(int i = 0;i < n;++i){
        scanf("%d", &temp);
        p.push(temp);
    }
    int ans = 0;
    for(int i = 1;i < n;++i){
        temp = (int)p.top();
        p.pop();
        temp += (int)p.top();
        p.pop();
        ans += temp;
        p.push(temp);
    }
    printf("%d", ans);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

还有糕手

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

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

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

打赏作者

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

抵扣说明:

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

余额充值