求解最优化的问题常常会有一系列的步骤,而每个步骤往往会面临着选择。
而贪心算法就是这样的算法,它在每一步都做出最优解,也就是说,它总是做出局部最优解,寄希望于通过局部最优解来获得全局最优解。
我们首先举个栗子:调度竞争资源来完成任务,比如我们要利用有限的资源(如一个阶梯教室)来实现任务(举办活动),每个活动的时间不一样,分别有开始时间和就结束时间,拿如何得到最优解呢?
我们看到虽然用动态规划来求解活动选择问题,但是并不需要这么做,我们可以反复选择最早结束的活动,同时保留与此兼容的活动,重复这一过程,直到不再有剩余活动。我们选择了活动结束的最早时间,所以活动结束时间必然是单调递增的,所以我们只需要按照结束时间的单调递增顺序处理所有活动,直到活动结束,每个活动只考察一次。
定理:考虑任何非空子集问题S,a是S中最早结束的活动,所以a必然在S中某个最大兼容活动子集中。
贪心算法往往是这种自顶向下的设计,先做出一个选择,然后再求解下一个问题,而不是自底向上解出许多子问题,然后再做出选择。
在做贪心选择时,我们直接做出当前问题中看起来最优的解,而不是考虑到子问题的解,这也是贪心算法和动态规划的不同之处,在动态规划中,我们往往每一个步骤都求做一个选择,这个选择往往依赖于子问题的解。而在贪心算法中,我们总是做出当时看来最佳的选择,然后再求解剩下唯一的子问题。贪心算法做出选择时可能会依赖于之前的选择或者子问题的解,但是绝对不依赖于将来的选择或者子问题的解。就是我们前面所说的,一个动态规划问题是自底向上的,而一个贪心算法问题是自顶向下的。
下面是几道leetcode题,解法思路都是贪心算法,我用的是python:
1.分配饼干
Leetcode : 455. Assign Cookies (Easy)
Input: [1,2], [1,2,3]
Output: 2
题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
思路:因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。
假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干,利用贪心策略最终可以满足 k 个孩子。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。
class Solution:
def findContentChildren(self, g, s):
"""
:type g: List[int]
:type s: List[int]
:rtype: int
"""
g.sort()
s.sort()
i=j=res=0
while i<len(g) and j<len(s):
if g[i]<=s[j]:
res += 1
i += 1
j += 1
return res
2.投飞镖刺破气球
Leetcode : 452. Minimum Number of Arrows to Burst Balloons (Medium)
Input:
[[10,16], [2,8], [1,6], [7,12]]
Output:
2
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
思路:从左往右投飞镖,并且在每次投飞镖时满足以下条件:
1.左边已经没有气球了;
2.本次投飞镖能够刺破最多的气球。
class Solution:
def findMinArrowShots(self, points):
"""
:type points: List[List[int]]
:rtype: int
"""
points.sort(key = lambda x:(x[0],x[1]))
n = len(points)
if n == 0:return 0
S = points[0][0]
E = points[0][1]
res = 1
for i in range(1,n):
if E>points[i][1]:
E = points[i][1]
elif S<points[i][0]:
S = points[i][0]
if S>E:
res += 1
S = points[i][0]
E = points[i][1]
return res
3.股票的最大收益
Leetcode : 122. Best Time to Buy and Sell Stock II (Easy)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
input: [7,1,5,3,6,4]
output: 7
思路: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加加到收益中,从而在局部最优的情况下也保证全局最优。
class Solution:
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
profit = 0
for i in range(len(prices)-1):
if prices[i+1]>prices[i]:
profit += (prices[i+1]-prices[i])
return profit
4.种植花朵
Leetcode : 605. Can Place Flowers (Easy)
Input: flowerbed = [1,0,0,0,1], n = 1
Output: True
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。
示例 1:
input: flowerbed = [1,0,0,0,1], n = 1
output: True
思路:自己想吧,不难的
class Solution:
def canPlaceFlowers(self, flowerbed, n):
"""
:type flowerbed: List[int]
:type n: int
:rtype: bool
"""
flowerbed.append(0)
flowerbed.insert(0,0)
num = 0
res = 0
for i in range(len(flowerbed)):
if flowerbed[i] == 0:
num += 1
else :
num = 0
if num == 3:
res += 1
num = 1
return res>=n