贪心算法是一种常见的算法思想,其特点就是通过每次都选择局部最优来达到实现全局最优的目的。

例如让你从一堆钞票中拿走10张,你为了达到拿到最大金额的目的,必定会每次都拿走最大面额的钞票,这就是贪心算法的思想。

一、贪心算法的主要特点

在刷题过程中,贪心算法的题目一般没有固定的套路,所以其最大的难点就是如何实现局部最优。

而实现局部最优的方法也因题而异,有时候就是常识性的推导。

我们唯一可以做的,就是

手动模拟一下,感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。

二、贪心算法的经典题目(简单难度)

LeetCode题目 相关题目类型 题目类型分析 题目思路 相关链接
455 分发饼干(简单难度) 一对一,将不同尺寸的饼干分给不同胃口的孩子,尽可能满足最多孩子的胃口 小尺寸饼干分给小胃口孩子 https://leetcode-cn.com/problems/combinations/
1005 K次取反后最大化的数组和(简单难度) 将数组中的数进行k次取相反数后数组和最大,可以对同一个数反复取相反数。 数组中的负数取反,负数的绝对值越大优先级越高;然后反复取同一个最小的非负数(实现上需要将数组按绝对值大小排序) https://leetcode-cn.com/problems/maximize-sum-of-array-after-k-negations/
860 柠檬水找零(简单难度) 付钱找零,判断是否可以全部正确找零 多种找零情况下优先找面额大的钞票 https://leetcode-cn.com/problems/lemonade-change/

1、455 分发饼干

该题目的目标是满足尽可能多的孩子的胃口,那么对于每个孩子就是尽可能用最小尺寸的饼干来其胃口,即尽可能做到大饼干喂给大胃口孩子,小饼干喂给小胃口孩子。

那么我们的代码肯定要先将孩子按照胃口大小从小到大排序,将饼干按尺寸大小从小到大排序,即:

sort(g.begin(),g.end());
sort(s.begin(),s.end());

接下来就是遍历每个孩子,看当前饼干尺寸是否满足其胃口,满足则更新当前孩子和饼干并计数,不满足则更新当前饼干,总的算法如下:

int findContentChildren(vector<int>& g, vector<int>& s) {
        if(s.size()==0){
            return 0;
        }
        //孩子们的胃口排序,用最小的饼干满足尽可能多的孩子
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int j = 0; // 当前饼干序号
        int count = 0;
        // 遍历每个孩子
        for(int i=0;i<g.size();){
            // 如果当前孩子胃口小于等于当前饼干尺寸
            if(g[i]<=s[j]){
                count++;//结果+1
                i++;//更新当前孩子
                //更新当前饼干
                j++;
                if(j>=s.size()){
                    break;
                }
            }
            // 如果当前孩子胃口大于当前饼干尺寸
            else{
                //更新当前饼干
                j++;
                if(j>=s.size()){
                    break;
                }
            }
        }
        return count;
    }

整理代码后,得:

int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int index = 0;
        for(int i = 0;i < s.size();++i){
            if(index < g.size() && g[index] <= s[i]){
                index++;
            }
        }
        return index;
    }

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

该题目是将数组中的数进行k次取相反数后数组和最大,可以对同一个数反复取相反数。

根据题意可知,使最后数组和最大的策略是:

1、首先将数组中的负数取反,负数的绝对值越大优先级越高;

2、如果此时数组中负数全取反后,k依旧不为0:

​ 如果k是偶数,则不用管,对同一个数进行偶数次取反还是原数

​ 如果k是奇数,则将此时数组中最小的数取反即可。

我们的策略有了,接下来就是实现,首先需要将数组按绝对值从大到小排列,这样就可以对负数进行取反;

然后判断k是奇数或者偶数即可:

static bool cmp(int a, int b) {
        return abs(a) > abs(b); //大于表示从大到小
    }
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        // 将数组按照绝对值进行从小到大的排序
        sort(nums.begin(), nums.end(), cmp);
        // 遍历数组,将负数变为整数,同时K--
        for (int i = 0; i <nums.size(); i++) { 
            if (nums[i] < 0 && k > 0) {
                nums[i] *= -1;
                k--;
            }
        }
        // 如果此时k依旧为奇数,则将最小的非负数反复转换为其相反数即可
        if (k % 2 == 1) {
            nums[nums.size() - 1] *= -1; 
        }
        int result = 0;
        // 求最大数组和
        for (int a : nums) {
            result += a; 
        }
        return result;
    }

本题的难点可能就是如何将数组按照想要的按绝对值大小进行排序。

3、860 柠檬水找零

有如下三种情况:

  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
bool lemonadeChange(vector<int>& bills) {
        int five_count = 0;//5美元的数量
        int ten_count = 0;//10美元的数量
        // 遍历账单
        for(int bill:bills){
            // 如果当前顾客付了5美元
            if(bill==5){
                five_count++;
            }
            // 如果当前顾客付了10美元
            else if(bill==10){
                ten_count++;
                // 如果有1张以上的5元零钱
                if(five_count>0){
                    five_count--;
                }
                else{
                    return false;
                }
            }
            // 如果当前顾客付了20美元
            else if(bill==20){
                // 如果同时有5元和10元的零钱
                if(ten_count>0&&five_count>0){
                    ten_count--;
                    five_count--;
                }
                // 如果只有3张以上的5元零钱
                else if(five_count>=3){
                    five_count-=3;
                }
                else{
                    return false;
                }
            }
        }
        return true;
    }