目录
贪心算法介绍
贪心算法(greedy algorithm)是一种算法思想,并不是具体的算法,常用来指导我们设计具体的算法和编码。
用贪心算法的步骤:
- 第一步,看到这类问题的时候,首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
- 第二步,尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据。
- 第三步,举几个例子,看下贪心算法产生的结果是否是最优的。
缺点:贪心算法并不总能给出最优解,特别是在前面的选择会影响后面的选择时可能会无缘全局最优解。
贪心算法例子
1. 背包
假设有一个可以容纳100kg物品的背包,我们有以下5种豆子,每种豆子的总量和总价值都各不相同。为了让背包中所装物品的总价值最大,如何选择在背包中装哪些豆子?每种豆子又该装多少呢?
- 限制值:重量
- 期望值:价值
- 贪心算法的选择策略:优先选择同等重量下价值更高的,也即单价更高的。单价从高到低排,依次是:黑豆、绿豆、红豆、青豆、黄豆,所以可以往背包里装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个区间里选出一部分区间,这部分区间两两不相交(端点相交的情况不算相交),最多能选出多少个区间呢?
- 限制值:区间左右端点
- 期望值:区间个数
- 贪心算法选择策略:优先选择左端点不与前序已选择区间重合、右端点尽量小的区间
5. 区间覆盖的延伸:任务调度、教师排课
贪心算法经典应用
1. 霍夫曼编码
霍夫曼编码(Huffman Coding)是一种高效编码方法,广泛用于无损数据压缩中,压缩率通常在20%~90%之间。
霍夫曼编码是一种不等长编码,根据贪心的思想,对出现频率多的字符用短编码,对出现频率少的字符用长编码。为了避免解压缩过程中的歧义,不会出现一个编码是另一个编码的前缀的情况。
其中,按不同频率编码成不同长度用到了优先级队列(大顶堆,大指的是频率):
- 先按频率排序。我们把每个字符看作一个节点,并且附带着把频率放到优先级队列中。
- 构建频率的优先级队列。我们从队列中取出频率最小的两个节点 A、B,然后新建一个节点 C,把频率设置为两个节点的频率之和,并把这个新节点 C 作为节点 A、B 的父节点。最后再把 C 节点放入到优先级队列中。重复这个过程,直到队列中没有数据。
- 霍夫曼编码。我们给每一条边加上画一个权值,指向左子节点的边我们统统标记为 0,指向右子节点的边,我们统统标记为 1,那从根节点到叶节点的路径就是叶节点对应字符的霍夫曼编码。
例如对于如下字符的huffman编码:
编码过程如下:
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 个人总的等待时间最短?
- 限制值:个人服务时长
- 期望值:总等待时间
- 贪心算法的选择策略:优先选择让别人等待时间短的,也即个人服务时长短的。