A. 贪心算法总结
做了这10道题,其实发现贪心算法没有什么规律,要说有什么共同特点就是都是由局部最优从而推出全局最优,每个题基本上都要考虑其局部最优是什么,其全局最优是什么,所以虽然都用到了贪心算法的思想,但是题与题之间又没有什么规律可言。
现在把这10道题的思路总结一下(总结主要以我的主观看法在写,可能别人看会不知道我在说什么)
1.分发饼干:
https://programmercarl.com/0455.%E5%88%86%E5%8F%91%E9%A5%BC%E5%B9%B2.html
思路:想要完成最多的小孩满足,那么就得最小的饼干给胃口最小的小孩
这里的贪心思想,
局部最优就是尽可能让一个饼干喂饱一个
全局最优就是最多的小孩满足
2.摆动序列:
https://programmercarl.com/0376.%E6%91%86%E5%8A%A8%E5%BA%8F%E5%88%97.html
思路:这里要找到最长的摆动序列,那么其实就是找那些波峰波谷,如图所示
可以看出来,在到达波峰波谷的路上有几个数字不会影响什么,可以直接去掉。
那么这里的局部最优就是把单调坡上的点删掉,保留最多的波峰波谷
全局最优就是得到对多的波峰波谷,即最长的摆动序列
3.最大子序和
https://programmercarl.com/0053.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C.html
这道题显然可以暴力解出来,即列出所有子序和,找出最大的,不过计算量会比贪心大很多。
这里主要介绍贪心解的思想:
想要得到最大子序和,就得保证每次相加时,相加后不能为负数,因为负数继续往下加一定是拉低总和的,那么我们当加成到负数时就重新从下个数开始加,并实时记录最大的子序和,这样一遍循环就能得出最大子序和。
局部最优:加成负数就立刻停止,并从下个元素重新开始
全局最优:得到最大子序和
4.买卖股票的最佳时机II
https://programmercarl.com/0122.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BAII.html
思路:想要得到最大利润,那就要低价买入高价卖出,那么怎样的买卖才能得到最大利润呢。
这里就体现出贪心算法的“贪”字(我猜的),这道题贪在哪呢,贪在只要有利可图就去做,即只要今天买入的价钱比明天卖出的价钱底,即有利可图,那么我就去做,做就是在今天买入,在明天卖出。
局部最优:得到每天的最大正利润
全局最优:得到最大利润
5.跳跃游戏
https://programmercarl.com/0055.%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F.html
思路:每个数组的元素代表的是可以跳的最远下标,那么我们只要使那个最远下标包含最后一个下标就是可以跳到,那么我们每跳到一个位置就要更新那个可以跳的范围,即可以跳到的最远下标。
局部最优:每次跳跃都得出最远的跳跃范围
全局最优:最后能跳到的最大范围
6.跳跃游戏II
https://programmercarl.com/0045.%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8FII.html
思路:这道题要得到最小的跳跃数,其实只要保证跳的是位置是可以跳范围内更新最远范围的位置就可以了。
为什么这么说呢?以题例来说:
我们刚开始在‘0’的位置,我们能跳到‘1’和‘2’的位置,那么我们怎么跳呢?可以看到跳到‘1’之后更新的最大范围是‘4’,跳到‘2’之后更新的最大范围是‘3’,所以我们就跳‘2’了,因为跳‘1’之后更新的最大可跳范围更大包含了跳‘2’的最大可跳范围,那么肯定是跳‘3’最优呀,这里就体现了局部最优的思想。
局部最优:每次跳后,更新的最大可调范围最大
全局最优:跳跃次数最少
7.K次取反后最大化的数组和
https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html
思路:想要得到最大数组和,我们就可以想到怎样做呢?
一,尽可能保证负数最少
二,负数绝对值大的优先变正
三,正数绝对值小的优先变负,有零变零
本着这三条原则做,就能做出来。
那么这道题体现了什么贪心思想呢?
我感觉,前面那三条都是贪心中‘贪’的体现
在负数中,局部最优就是:绝对值大的负数优先变正
在正数中,局部最优就是:绝对值小的正数变负,有零变零
得到的全局最优:数组和最大
8.加油站
https://programmercarl.com/0134.%E5%8A%A0%E6%B2%B9%E7%AB%99.html
思路:首先可以想到这道题是可以暴力解出来了,即分别以每个加油站为起点,得出可以跑一圈的加油站
那么贪心思想做,该怎么做呢,首先可以想到,如果以一个1点为起点当跑着跑着跑到3,油变为负数时,那么说明以这个起点是不行的,但是以2或3为起点行不行呢?答案肯定是不行的,因为1跑到3,油变为负,说明1~3的gas=0的,所以可以得出,如果1~3油数变为负数,那么由2~3油数肯定也为负数。
所以这里就可以得出,如果经过几个加油站油数变为负了,那么起点就更新为这一段路的下个加油站跑
局部最优:油量一旦为负,就从下个加油站重新跑
全局最优:得出可以跑一圈的加油站起点
9.分发糖果
https://programmercarl.com/0135.%E5%88%86%E5%8F%91%E7%B3%96%E6%9E%9C.html
思路:每个孩子至少一个,如果一个孩子比他旁边的孩子优秀,就要比他旁边的糖果多,这道题一旦两边都考虑很容易顾此失彼,所以我们就定义两个循环,分别从左到右,从右到左去考虑,只要更优秀则比他旁边的多1,如果已经多了就不用变了。
局部最优:保证优秀的孩子比他旁边的孩子糖果多
全局最优:满足题中条件,至少要发的糖果
10.柠檬水找零
https://programmercarl.com/0860.%E6%9F%A0%E6%AA%AC%E6%B0%B4%E6%89%BE%E9%9B%B6.html
思路:我们在找零时要遵守的规则一定是:
5 得5
10 得10减5
15 得15,优先减一个10减一个5 如果10块没有则减三个5
局部最优:以最少用的5块的方式找零
全局最优:得到找零能否进行下去
B. 五大常用算法之一:贪心算法
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。比如, 求最小生成树的Prim算法和Kruskal算法都是漂亮的贪心算法 。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
贪心策略:选取价值最大者。反例:
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
(3)贪心策略:选取单位重量价值最大的物品。反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。但是果在条件中加一句当遇见单位价值相同的时候,优先装重量小的,这样的问题就可以解决.
所以需要说明的是,贪心算法可以与随机化算法一起使用,具体的例子就不再多举了。(因为这一类算法普及性不高,而且技术含量是非常高的,需要通过一些反例确定随机的对象是什么,随机程度如何,但也是不能保证完全正确,只能是极大的几率正确)。
C. 算法09-贪心算法
贪心算法与动态规划的不同在于它对每个子问题的解决方案都作出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
很多情况下,可以在某一步用贪心算法,全局再加一个搜索或递归或动态规划之类
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码等。然而对于工程和生活中的问题,贪心法一般不能得到我们所要求的答案。
一单一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
当硬币可选集合固定:Coins = [20,10,5,1],求最少几个硬币可以拼出总数。比如total=36。
36 - 20 = 16 20
16 - 10 = 6 20 10
6 - 5 = 1 20 10 5
1 - 1 = 0 20 10 5 1
前面这些硬币依次是后面硬币的整数倍,可以用贪心法,能得到最优解,
贪心法的反例
非整除关系的硬币,可选集合:Coins = [10,9,1],求拼出总数为18最少需要几个硬币?
最优化算法:9 + 9 = 18 两个9
贪心算法:18 - 10 = 8 - 1 - 1 - 1 - 1 - 1 - 1 - 1 - 1 = 0 八个1
简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。这种子问题最优解成为最优子结构。
贪心算法与动态规划的不同在于它对每个子问题的最终方案都作出选择,不能回退。
动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
提示:
1 <= g.length <= 3 * 104
0 <= s.length <= 3 * 104
1 <= g[i], s[j] <= 231 - 1
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:
假设你总是可以到达数组的最后一个位置。
移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。
想要达到这样的效果,只要让移动下标,最大只能移动到nums.size - 2的地方就可以了。
因为当移动下标指向nums.size - 2时:
如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图:
如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图:
机器人在一个无限大小的 XY 网格平面上行走,从点 (0, 0) 处开始出发,面向北方。该机器人可以接收以下三种类型的命令 commands :
-2 :向左转 90 度
-1 :向右转 90 度
1 <= x <= 9 :向前移动 x 个单位长度
在网格上有一些格子被视为障碍物 obstacles 。第 i 个障碍物位于网格点 obstacles[i] = (xi, yi) 。
机器人无法走到障碍物上,它将会停留在障碍物的前一个网格方块上,但仍然可以继续尝试进行该路线的其余部分。
返回从原点到机器人所有经过的路径点(坐标为整数)的最大欧式距离的平方。(即,如果距离为 5 ,则返回 25 )
注意:
北表示 +Y 方向。
东表示 +X 方向。
南表示 -Y 方向。
西表示 -X 方向。
示例 1:
输入:commands = [4,-1,3], obstacles = []
输出:25
解释:
机器人开始位于 (0, 0):
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
示例 2:
输入:[5,5,10]
输出:true
示例 3:
输入:[10,10]
输出:false
示例 4:
输入:[5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
D. 哈夫曼编码(贪心算法)
参考: 哈夫曼编码
哈夫曼编码是一种十分有效的编码方法,广泛应用于 数据压缩 中
通过采用 不等长 的编码方式,根据 字符频率的不同 ,选择 不同长度的编码 ,对频率 越高 的字符采用 越短 的编码实现数据的高度压缩。
这种对频率越高的字符采用越短的编码来编码的方式应用的就是贪心算法的思想。
下面看一个例子:
假如我们有一个包含1000个字符的文件,每个字符占1个byte(1byte=8bits),则存储这100个字符一共需要8000bits。这还是有一些大的
那我们统计一下这1000个字符中总共有多少种字符,原来需要8bit来表示一个字符,如果使用更少的位数来表示这些字符,则可以减少存储空间。
假设这1000个字符中总共有a、b、c、d、e、f共6种字符,使用使用3个二进制位来表示的话,存储这1000个字符就只需要3000bits,比原来更节省存储空间。
或许还可以再压缩一下:
根据字符出现的 频率 给与字符 不等长 的编码,频率越高的字符编码越短,频率越低的字符编码越长。
它不能像等长编码一样直接按固定长度去读取二进制位,翻译成字符,为了能够准确读取翻译字符,它要求一个字符的编码不能是另外一个字符的前缀。
假设a、b、c、d、e、f这6个字符出现的频率依次降低,则我们可以给与他们这样的编码
假如字符的出现频率如图所示,按照这样的编码表示的话,总位数如图,一共2100bits,更加节省空间了
贪心策略:频率小的字符,优先入队。
步骤:
1.将每一个字符作为节点,以出现频率大小作为权重,将其都放入 优先队列 中(一个最小堆);
2.每次出队两个节点并创建一个父节点,使其权值为刚刚出队的节点的权值和,并且为两个节点的父节点(合并)。然后将这个树入队。
3.重复操作2,直到队列中只有一个元素(此时这个元素表示形式应该为一个树)时,完成创建。
创建好了树,该怎么编码呢?
我们对一个哈夫曼树,从父节点开始的所有节点,往左边标0,右边标1。那么到达叶子节点的顺次编码就可以找到了。
C:字符集合
Q:优先队列
EXTRACT-MIN:传入一个队列,出队最小的元素
INSERT:将z插入到Q中
当for循环结束之后,此时队列中只有一个元素,就是我们需要的哈夫曼树,最后返回此树即可。
假设T树已经是一个最优的树,假设x、y的频率小于等于最低处的a、b,然后交换x、a,y、b。
计算代价是否发生变化。
比如这里比较 T 变成 T ’ 后代价是否变化,发现代价变小或不变。
同理T’到T’’,又因为T本来假设就是最优的,所以只能相等
所以T’’也应该符合条件,即贪婪算法,每次取最小的两个节点出来这种做法是正确的
E. (三) 贪心算法
贪心算法的思想非常简单且算法效率很高,在一些问题的解决上有着明显的优势。
假设有3种硬币,面值分别为1元、5角、1角。这3种硬币各自的数量不限,现在要找给顾客3元6角钱,请问怎样找才能使得找给顾客的硬币数量最少呢?
你也许会不假思索的说出答案:找给顾客3枚1元硬币,1枚5角硬币,1枚1角硬币。其实也可以找给顾客7枚5角硬币,1枚1角硬币。可是在这里不符合题意。在这里,我们下意识地应用了所谓贪心算法解决这个问题。
所谓贪心算法,就是 总是做出在当前看来是最好的选择(未从整体考虑) 的一种方法。以上述的题目为例,为了找给顾客的硬币数量最少,在选择硬币的面值时,当然是尽可能地选择面值大的硬币。因此,下意识地遵循了以下方案:
(1)首先找出一个面值不超过3元6角的最大硬币,即1元硬币。
(2)然后从3元6角中减去1元,得到2元6角,再找出一个面值不超过2元6角的最大硬币,即1元硬币。
(3)然后从2元6角中减去1元,得到1元6角,再找出一个面值不超过1元6角的最大硬币,即1元硬币。
(4)然后从1元6角中减去1元,得到6角,再找出一个面值不超过6角的最大硬币,即5角硬币。
(5)然后从6角中减去5角,得到1角,再找出一个面值不超过1角的最大硬币,即1角硬币。
(6)找零钱的过程结束。
这个过程就是一个典型的贪心算法思想。
贪心策略总是做出在当前看来是最优的选择,也就是说贪心策略并不是从整体上加以考虑,它所做出的选择只是在某种意义上的 局部最优解 ,而许多问题自身的特性决定了该问题运用贪心策略可以得到最优解或较优解。(注:贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题它能产生整体最优解。但其解必然是最优解的很好近似解。)
贪心算法没有固定的算法框架,算法设计的关键是 贪心策略的选择 。选择的贪心策略必须具备无后效性。
贪心策略 适用的前提 是:
严格意义上讲,要使用贪心算法求解问题,该问题应当具备以下性质:
注意 :对于一个给定的问题,往往可能有好几种量度标准。初看起来,这些量度标准似乎都是可取的,但实际上,用其中的大多数量度标准作贪婪处理所得到该量度意义下的最优解并不是问题的最优解,而是次优解。
因此, 选择能产生问题最优解的最优量度标准是使用贪婪算法的核心 。
实际上,贪心算法 适用的情况很少 。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
最优解问题大部分都可以拆分成一个个的子问题(多阶段决策问题),把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,大部分情况下这是不可行的。
贪心算法和动态规划本质上是对子问题树的一种修剪,两种算法要求问题都具有的一个性质就是子问题最优性(组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的)。
动态规划方法代表了这一类问题的一般解法, 自底向上 构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。
而贪心算法是动态规划方法的一个特例,可以证明每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始( 自顶向下 ),选择最优的路,一直走到底就可以了。
一个问题分为多个阶段,每个阶段可以有n种决策,各个阶段的决策构成一个决策序列,称为一个策略。
这两种算法都是选择性算法,在进行决策的选择时:
前提是这个问题得具有贪心选择性质,需要证明(数学归纳法(第一、第二)),如果不满足那就只能使用动态规划解决。(一旦证明贪心选择性质,用贪心算法解决问题比动态规划具有更低的时间复杂度和空间复杂度。)
从范畴上来看:
Greedy ⊂ DP ⊂ Searching (贪心是动规的特例)
即所有的贪心算法问题都能用DP求解,更可以归结为一个搜索问题,反之不成立。
贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,这使得算法在编码和执行的过程中都有着一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。但是贪心算法并不是对所有的问题都能得到整体最优解或最理想的近似解,与回溯法等比较,它的适用区域相对狭窄许多,因此正确地判断它的应用时机十分重要。
一步一步地进行,常 以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况 ,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。
它采用 自顶向下 ,以 迭代 的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以 贪心法不需要回溯 。
【问题描述】
马的遍历问题。在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条最短路径。
【贪心算法】
其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,‘出口’的意思是在这些子结点中它们的可行子结点的个数,也就是‘孙子’结点越少的越优先跳,为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。
F. 程序员算法基础——贪心算法
贪心是人类自带的能力,贪心算法是在贪心决策上进行统筹规划的统称。
比如一道常见的算法笔试题---- 跳一跳 :
我们自然而然能产生一种解法:尽可能的往右跳,看最后是否能到达。
本文即是对这种贪心决策的介绍。
狭义的贪心算法指的是解最优化问题的一种特殊方法,解决过程中总是做出当下最好的选择,因为具有最优子结构的特点,局部最优解可以得到全局最优解;这种贪心算法是动态规划的一种特例。 能用贪心解决的问题,也可以用动态规划解决。
而广义的贪心指的是一种通用的贪心策略,基于当前局面而进行贪心决策。以 跳一跳 的题目为例:
我们发现的题目的核心在于 向右能到达的最远距离 ,我们用maxRight来表示;
此时有一种贪心的策略:从第1个盒子开始向右遍历,对于每个经过的盒子,不断更新maxRight的值。
贪心的思考过程类似动态规划,依旧是两步: 大事化小 , 小事化了 。
大事化小:
一个较大的问题,通过找到与子问题的重叠,把复杂的问题划分为多个小问题;
小事化了:
从小问题找到决策的核心,确定一种得到最优解的策略,比如跳一跳中的 向右能到达的最远距离 ;
在证明局部的最优解是否可以推出全局最优解的时候,常会用到数学的证明方式。
如果是动态规划:
要凑出m元,必须先凑出m-1、m-2、m-5、m-10元,我们用dp[i]表示凑出i元的最少纸币数;
有 dp[i]=min(dp[i-1], dp[i-2], dp[i-5], dp[i-10]) + 1 ;
容易知道 dp[1]=dp[2]=dp[5]=dp[10]=1 ;
根据以上递推方程和初始化信息,可以容易推出dp[1~m]的所有值。
似乎有些不对? 平时我们找零钱有这么复杂吗?
从贪心算法角度出发,当m>10且我们有10元纸币,我们优先使用10元纸币,然后再是5元、2元、1元纸币。
从日常生活的经验知道,这么做是正确的,但是为什么?
假如我们把题目变成这样,原来的策略还能生效吗?
接下来我们来分析这种策略:
已知对于m元纸币,1,2,5元纸币使用了a,b,c张,我们有a+2b+5c=m;
假设存在一种情况,1、2、5元纸币使用数是x,y,z张,使用了更少的5元纸币(z<c),且纸币张数更少(x+y+z<a+b+c),即是用更少5元纸币得到最优解。
我们令k=5*(c-z),k元纸币需要floor(k/2)张2元纸币,k%2张1元纸币;(因为如果有2张1元纸币,可以使用1张2元纸币来替代,故而1元纸币只能是0张或者1张)
容易知道,减少(c-z)张5元纸币,需要增加floor(5*(c-z)/2)张2元纸币和(5*(c-z))%2张纸币,而这使得x+y+z必然大于a+b+c。
由此我们知道不可能存在使用更少5元纸币的更优解。
所以优先使用大额纸币是一种正确的贪心选择。
对于1、5、7元纸币,比如说要凑出10元,如果优先使用7元纸币,则张数是4;(1+1+1+7)
但如果只使用5元纸币,则张数是2;(5+5)
在这种情况下,优先使用大额纸币是不正确的贪心选择。(但用动态规划仍能得到最优解)
如果是动态规划:
前i秒的完成的任务数,可以由前面1~i-1秒的任务完成数推过来。
我们用 dp[i]表示前i秒能完成的任务数 ;
在计算前i秒能完成的任务数时,对于第j个任务,我们有两种决策:
1、不执行这个任务,那么dp[i]没有变化;
2、执行这个任务,那么必须腾出来(Sj, Tj)这段时间,那么 dp[i] = max(dp[i], dp[ S[j] ] ) + 1 ;
比如说对于任务j如果是第5秒开始第10秒结束,如果i>=10,那么有 dp[i]=max(dp[i], dp[5] + 1); (相当于把第5秒到第i秒的时间分配给任务j)
再考虑贪心的策略,现实生活中人们是如何安排这种多任务的事情?我换一种描述方式:
我们自然而然会想到一个策略: 先把结束时间早的兼职给做了!
为什么?
因为先做完这个结束时间早的,能留出更多的时间做其他兼职。
我们天生具备了这种优化决策的能力。
这是一道 LeetCode题目 。
这个题目不能直接用动态规划去解,比如用dp[i]表示前i个人需要的最少糖果数。
因为(前i个人的最少糖果数)这种状态表示会收到第i+1个人的影响,如果a[i]>a[i+1],那么第i个人应该比第i+1个人多。
即是 这种状态表示不具备无后效性。
如果是我们分配糖果,我们应该怎么分配?
答案是: 从分数最低的开始。
按照分数排序,从最低开始分,每次判断是否比左右的分数高。
假设每个人分c[i]个糖果,那么对于第i个人有 c[i]=max(c[i-1],c[c+1])+1 ; (c[i]默认为0,如果在计算i的时候,c[i-1]为0,表示i-1的分数比i高)
但是,这样解决的时间复杂度为 O(NLogN) ,主要瓶颈是在排序。
如果提交,会得到 Time Limit Exceeded 的提示。
我们需要对贪心的策略进行优化:
我们把左右两种情况分开看。
如果只考虑比左边的人分数高时,容易得到策略:
从左到右遍历,如果a[i]>a[i-1],则有c[i]=c[i-1]+1;否则c[i]=1。
再考虑比右边的人分数高时,此时我们要从数组的最右边,向左开始遍历:
如果a[i]>a[i+1], 则有c[i]=c[i+1]+1;否则c[i]不变;
这样讲过两次遍历,我们可以得到一个分配方案,并且时间复杂度是 O(N) 。
题目给出关键信息:1、两个人过河,耗时为较长的时间;
还有隐藏的信息:2、两个人过河后,需要有一个人把船开回去;
要保证总时间尽可能小,这里有两个关键原则: 应该使得两个人时间差尽可能小(减少浪费),同时船回去的时间也尽可能小(减少等待)。
先不考虑空船回来的情况,如果有无限多的船,那么应该怎么分配?
答案: 每次从剩下的人选择耗时最长的人,再选择与他耗时最接近的人。
再考虑只有一条船的情况,假设有A/B/C三个人,并且耗时A<B<C。
那么最快的方案是:A+B去, A回;A+C去;总耗时是A+B+C。(因为A是最快的,让其他人来回时间只会更长, 减少等待的原则 )
如果有A/B/C/D四个人,且耗时A<B<C<D,这时有两种方案:
1、最快的来回送人方式,A+B去;A回;A+C去,A回;A+D去; 总耗时是B+C+D+2A (减少等待原则)
2、最快和次快一起送人方式,A+B先去,A回;C+D去,B回;A+B去;总耗时是 3B+D+A (减少浪费原则)
对比方案1、2的选择,我们发现差别仅在A+C和2B;
为何方案1、2差别里没有D?
因为D最终一定要过河,且耗时一定为D。
如果有A/B/C/D/E 5个人,且耗时A<B<C<D<E,这时如何抉择?
仍是从最慢的E看。(参考我们无限多船的情况)
方案1,减少等待;先送E过去,然后接着考虑四个人的情况;
方案2,减少浪费;先送E/D过去,然后接着考虑A/B/C三个人的情况;(4人的时候的方案2)
到5个人的时候,我们已经明显发了一个特点:问题是重复,且可以由子问题去解决。
根据5个人的情况,我们可以推出状态转移方程 dp[i] = min(dp[i - 1] + a[i] + a[1], dp[i - 2] + a[2] + a[1] + a[i] + a[2]);
再根据我们考虑的1、2、3、4个人的情况,我们分别可以算出dp[i]的初始化值:
dp[1] = a[1];
dp[2] = a[2];
dp[3] = a[2]+a[1]+a[3];
dp[4] = min(dp[3] + a[4] + a[1], dp[2]+a[2]+a[1]+a[4]+a[2]);
由上述的状态转移方程和初始化值,我们可以推出dp[n]的值。
贪心的学习过程,就是对自己的思考进行优化。
是把握已有信息,进行最优化决策。
这里还有一些收集的 贪心练习题 ,可以实践练习。
这里 还有在线分享,欢迎报名。
G. 贪心算法
平面点集三角剖分的贪心算法要求三角剖分后边的总长度尽可能小。算法的基本思想是将所有的两点间距离从小到大排序,依次序每次取一条三角剖分的边,直至达到要求的边数。以下是两种贪心算法的主要步骤。
3.2.2.1 贪心算法1
第一步:设置一个记录三角剖分中边的数组T。
第二步:计算点集S中所有点对之间的距离d(pi,pj),1≤i,j≤n,i≠j,并且对距离从小到大进行排序,设为d1,d2,…,dn(n-1)/2,相应的线段记为e1,e2,…,en(n-1)/2,将这些线段存储在数组E中。
第三步:从线段集E中取出长度最短的边e1存到T中作为三角剖分的第一条边,此时k=1。
第四步:依次从E中取出长度最短的边ek,与T中已有的边进行求交运算,如果不相交则存到T中,并从E中删除ek。这一步运行到S中没有边为止,即至k=n(n-1)/2。
第五步:输出T。
该算法中,第二步需要计算n(n-1)/2次距离,另外距离排序需要O(n2lgn)次比较。T中元素随第四步循环次数的增加而增加,因此向T中加入一条新边所需要的判定两条线段是否相交的次数也随之增加。如果第四步的前3n-6次循环后已经构成点集的三角剖分,那么第四步循环所需要的判定两条线段是否相交的次数为
1+2+…+3n-7+(3n-6)×(n(n-1)/2-(3n-6))=O(n3)
在常数时间内可以判定两条线段是否相交,因此该算法的时间复杂性为O(n3)。
3.2.2.2 贪心算法2
第一步:求点集的凸壳,设凸壳顶点为p1,p2,…,pm,凸壳的边为e1,e2,…,em。并将凸壳顶点按顺序连接成边的ei加入T(三角剖分的边集合),并且ei的权值被赋为1。凸壳内点的集合为S1={pm+1,pm+2,…,pn}。
第二步:从内部点S1中任取一点pi,求与pi距离最近的点pj,将线段 存入T。
第三步:求与pj距离最近的点(除点pi外),设为pk,并将线段 加入T,并将这些边的权值设为1,而wij、wjk和wki的值加1,即为2。边的权值为2则表示该边为两个三角形共有。
第五步:对权值为1的边(除e1,e2,…,em外)的两个端点分别求与其距离最近的点,并将其连线(得到新的三角形)加入T,新三角形边的权值加1。
第六步:对权值为1的边重复上一步,当一条边被使用一次其权值增加1,直到所有边的权值均为2为止(除e1,e2,…,em外)。
贪心算法2中,第一步耗费O(nlgn);第二步需要计算n-1次距离与n-2次比较;第三步求pk要计算n-2次的距离与n-3次比较;第四步要进行(n-3)×3次的距离计算及(n-4)×3次比较;第五步至多进行n-6次的距离计算与n-7次比较;第六步到第五步的循环次数不超过3n-9;因此整个贪心算法2的时间复杂性为
O(nlgn)+O(n)+O(n)+O(n)+(n-6)×(3n-9)=O(n2)