「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。
贪心算法
贪心算法,又称贪婪算法,其原理很简单:对问题求解的时候,总是做出对当前看来最好的选择。
我们通过一个例子来感受一下。
排课表
假设有如下课程表,你希望尽可能多的课程安排在某间教室上。
课程 | 开始时间 | 结束时间 |
---|---|---|
语文 | 9:00 | 10:00 |
数学 | 9:30 | 10:30 |
英语 | 10:00 | 11:00 |
物理 | 10:30 | 11:30 |
化学 | 11:00 | 12:00 |
你没法让这些课都安排在一间教室,因为有些课时间有冲突。
使用贪心算法,就可以轻松地得到解决方案。
- 选出结束最早的课,它就是要在这间教室上的第一堂课。
- 接下来,必须选择第一堂课结束后才开始的课,就是要在这间教室上的第二堂课。
- 重复上面的过程,直到选完。
最终,我们最多安排了三门课程:
课程 | 开始时间 | 结束时间 |
---|---|---|
语文 | 9:00 | 10:00 |
英语 | 10:00 | 11:00 |
化学 | 11:00 | 12:00 |
很多人说,这个算法太容易,太显而易见了,不可取。
但这正是贪心算法的优点,简单易行 ,每步都选择局部最优解,最终得到的就是全局最优解。
但是,贪心算法不一定在任何情况下都能拿到最优解,比如下面这个例子。
背包问题
假设一个背包可以装 35 磅的物品,尽可能地装入价值高的物品。
物品 | 重量 | 价值 |
---|---|---|
音响 | 30磅 | 3000元 |
电脑 | 20磅 | 2000元 |
吉他 | 15磅 | 1500元 |
如果采用贪心算法,直接装音响,占据了 30 榜空间,然后就装不下其他物品了,最终价值 3000 元,有 5 磅空间浪费了。
但如果装电脑和吉他,空间利用满了,最终价值 3500 元。
显然,在背包问题里,贪心算法无法拿到最优解。
但使用贪心算法,拿到的也比较接近最优解了,很多时候,我们不一定非要拿最优解,拿到一个近似值也够了。
下面再举一个拿近似值的例子。
旅行商问题(NP完全问题)
一位旅行商要游玩 5 个城市,如何游玩旅程会最短?
这个看似简单的问题,人来想大概捋一捋,最多拿出纸笔算一算,就能想出来。
但是计算机来解决这个问题的话,时间复杂度出其的高,为O(n!)
5 个城市就有 120 种不同的排列方式,6 个就是 720,7个就是 5040了!
这种算法很糟糕,如果我们采用贪心算法,情况会好一些。
随便选择一个出发的城市,每次找到较短路径,下一个城市就去还没去的城市里最近的。
这样的结果不一定是最短的,但是也多半比较接近最短的了。
这种类似的问题被称为 NP完全问题,世界上最聪明的一批人目前都没有解决,只能通过各种办法找近似值。
适用贪心算法的场景
与 分治法
类似,问题能够分解成子问题来解决。
分治法我在这篇文章, 写给算法初学者的分治法和快速排序(js) 有详细介绍。
子问题的最优解能递推到最终问题的最优解。这种子问题最优解称为最优子结构。
一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。
由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
贪心算法与动态规划的不同
贪心算法不一定能找到最优解,如果要找到最优解,就要用到动态规划了。
动态规划简单来说就是,对每一个子问题不一定选择最优解,可以把最优、次优和觉得不错的解存起来。
存起来之后,贪心算法是不能回退的,每个子问题都选当前最优解,就解决了。
而动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能,最后得到一个全局最优解。
动态规划每次做选择的时候会综合考虑几种方案,还会保存下来,往后面一步步走时,发现之前的方案选得不好,就回退回去,做一些调整,最后调整出来一个最优的解。
简单来说,贪心算法目光短浅,动态规划格局打开。
贪心算法leetcode初体验
122. 买卖股票的最佳时机 II
题目描述:给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。返回 你能获得的 最大 利润 。
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
复制代码
这道题有很多解法,这里只讨论贪心算法的思路。
局部最优:见好就收,见差就不动,不做任何长远打算。
解题步骤:
- 新建一个变量,用来统计总利润。
- 遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不交易。
- 遍历结束后,返回所有利润之和
const maxProfit = function (prices) {
let res = 0
for (let i = 1, len = prices.length; i < len; ++i) {
res += Math.max(0, prices[i] - prices[i - 1])
}
return res
}
复制代码
时间复杂度:O(n)
空间复杂度:O(1)
小结
在生活中,我们做决定如果都只选择当前的最优选择,可能会被人说目光短浅。
但很多时候,每次只选最优选择,积累得多了,也能走得很远。
在算法世界中,贪心算法不一定能找到最优解,但能比较简单地找到最优解的近似值,如果要找到最优解要付出非常大的代价(NP完全问题),那么使用贪心算法来得到一个近似值还是不错的。
贪心算法面试一般不考,但理解其思想,还是挺不错的,可以给我们很多思考。
这篇文章 介绍了贪心算法面试不考的理由,就是有点功利,hhh。
往期算法相关文章