数据结构与算法之美(十四)算法思想——贪心算法

目录

  • 贪心算法介绍
  • 贪心算法例子
    • 1. 背包
    • 2. 分糖果
    • 3. 钱币找零
    • 4. 区间覆盖
    • 5. 区间覆盖的延伸:任务调度、教师排课
  • 贪心算法经典应用
    • 1. 霍夫曼编码
    • 2. 最小生成树算法
    • 3. 最短路径算法
  • 课后思考

贪心算法介绍

贪心算法(greedy algorithm)是一种算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码。

用贪心算法的步骤:

  • 第一步,看到这类问题的时候,首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
  • 第二步,尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据。
  • 第三步,举几个例子,看下贪心算法产生的结果是否是最优的。

缺点:贪心算法并不总能给出最优解,特别是在前面的选择会影响后面的选择时可能会无缘全局最优解。

贪心算法例子

1. 背包

假设有一个可以容纳100kg物品的背包,我们有以下5种豆子,每种豆子的总量和总价值都各不相同。为了让背包中所装物品的总价值最大,如何选择在背包中装哪些豆子?每种豆子又该装多少呢?
数据结构与算法之美(十四)算法思想——贪心算法_第1张图片

  • 限制值:重量
  • 期望值:价值
  • 贪心算法的选择策略:优先选择同等重量下价值更高的,也即单价更高的。单价从高到低排,依次是:黑豆、绿豆、红豆、青豆、黄豆,所以可以往背包里装20kg黑豆、30kg绿豆、50kg红豆。

2. 分糖果

有m个糖果和n个孩子,要把糖果分给孩子吃,但糖果少孩子多(m < n),所以糖果只能分配给一部分孩子。每个糖果有大小si,每个孩子有需求量gi,只有当糖果大小大于等于孩子的需求量时这个孩子才能得到满足。如何分配糖果,能尽可能满足最多数量的孩子?

  • 限制值:糖果个数
  • 期望值:满足的孩子个数(从n里抽出一部分)
  • 贪心算法的选择策略:优先选择需求量小的孩子、对于一个孩子优先选择满足需求的较小的糖果。

3. 钱币找零

假设有1元、2元、5元、10元、20元、50元、100元面额的纸币,张数分别是ci,我们现在要用这些钱来支付K元,最少要用多少张纸币呢?

  • 限制值:金额K元
  • 期望值:纸币张数
  • 贪心算法选择策略:优先选择对金额贡献相等的情况下对期望值贡献最大的,也即优先选择面额较大的,先用最大面值支付,不够就继续用更小一点的面值,以此类推,最后剩下的用1元来补齐。

但在不同面额设置的情况下,用贪心算法并不一定是最优的,有时需要动态规划?

4. 区间覆盖

假设有n个区间,区间的起始端点和结束端点分别是[li, ri],从这n个区间里选出一部分区间,这部分区间两两不相交(端点相交的情况不算相交),最多能选出多少个区间呢?

  • 限制值:区间左右端点
  • 期望值:区间个数
  • 贪心算法选择策略:优先选择左端点不与前序已选择区间重合、右端点尽量小的区间
    数据结构与算法之美(十四)算法思想——贪心算法_第2张图片
    数据结构与算法之美(十四)算法思想——贪心算法_第3张图片

5. 区间覆盖的延伸:任务调度、教师排课

贪心算法经典应用

1. 霍夫曼编码

霍夫曼编码(Huffman Coding)是一种高效编码方法,广泛用于无损数据压缩中,压缩率通常在20%~90%之间。

霍夫曼编码是一种不等长编码,根据贪心的思想,对出现频率多的字符用短编码,对出现频率少的字符用长编码。为了避免解压缩过程中的歧义,不会出现一个编码是另一个编码的前缀的情况。

其中,按不同频率编码成不同长度用到了优先级队列(大顶堆,大指的是频率):

  • 先按频率排序。我们把每个字符看作一个节点,并且附带着把频率放到优先级队列中。
  • 构建频率的优先级队列。我们从队列中取出频率最小的两个节点 A、B,然后新建一个节点 C,把频率设置为两个节点的频率之和,并把这个新节点 C 作为节点 A、B 的父节点。最后再把 C 节点放入到优先级队列中。重复这个过程,直到队列中没有数据。
  • 霍夫曼编码。我们给每一条边加上画一个权值,指向左子节点的边我们统统标记为 0,指向右子节点的边,我们统统标记为 1,那从根节点到叶节点的路径就是叶节点对应字符的霍夫曼编码。

例如对于如下字符的huffman编码:
数据结构与算法之美(十四)算法思想——贪心算法_第4张图片
编码过程如下:

数据结构与算法之美(十四)算法思想——贪心算法_第5张图片

2. 最小生成树算法

Prim和Kruskal最小生成树算法

3. 最短路径算法

Dijkstra单源最短路径算法

课后思考

1. 在一个非负整数 a 中,我们希望从中移除 k 个数字,让剩下的数字值最小,如何选择移除哪 k 个数字呢?
https://leetcode-cn.com/problems/remove-k-digits/

  • 限制值:移除数字的个数
  • 期望值:值
  • 贪心算法的选择策略:从左往右找到第一个变为递减的位置i,并删去i-1的元素。
class Solution {
public:
    string removeKdigits(string num, int k) {
        /* 
        一、考查重点:
            1. 贪心算法:删除从左到右第一个不能保证单调递增的元素的左邻居
            2. 单调栈:用单调栈来维护结果,复杂度从O(nk)->O(n)
        
        二、具体实现:
            1. 维护一个单调栈,里面存结果,从左到右依次添加
            2. 从左到右遍历每一个元素(for),
            (1)如果出现栈顶元素>该元素,则不断pop栈顶元素,直到(while和条件)“k为0 || 栈为空 || 栈顶元素>该元素”
            (2)如果“栈为空 || 栈顶元素<=该元素”,添加进栈
            3. 特殊情况
            (1)如果遍历完,k还不为0,则从倒着删
            (2)如果栈不为空,要看下是否有前导0,有的话要删除
            (3)如果栈为空了,返回“0”
        
        三、复杂度:
            1. 时间复杂度:O(n)
            2. 空间复杂度:O(n)
        */

        vector<char> stack;
        for(auto & n: num)
        {
            if(stack.empty() || stack.back() <= n) stack.push_back(n);
            else
            {
                while(k && !stack.empty() && stack.back() > n)
                {
                    stack.pop_back();
                    --k;
                }
                stack.push_back(n);
            }
        }

        for(; k; --k)
        {
            if(!stack.empty()) stack.pop_back();
        }

        string res;
        bool isPrefixZero = true;
        for(auto & digit: stack)
        {
            if ((digit != '0') || (!isPrefixZero))
            {
                isPrefixZero = false;
                res += digit;
            }
        }

        if(res.empty()) return "0";
        return res;
    }
};

2. 假设有 n 个人等待被服务,但是服务窗口只有一个,每个人需要被服务的时间长度是不同的,如何安排被服务的先后顺序,才能让这 n 个人总的等待时间最短?

  • 限制值:个人服务时长
  • 期望值:总等待时间
  • 贪心算法的选择策略:优先选择让别人等待时间短的,也即个人服务时长短的。

你可能感兴趣的:(数据结构与算法之美,算法,数据结构)