贪心算法解题套路框架
本文讲解的例题
| LeetCode | 力扣 | 难度 |
|---|---|---|
| 45. Jump Game II | 45. 跳跃游戏 II | 🟠 |
| 55. Jump Game | 55. 跳跃游戏 | 🟠 |
一句话总结
回溯算法的剪枝优化是提前排除不可能的答案,使树结构尽可能小,最终的算法复杂度一般是指数级别;动态规划的备忘录优化是为了避免重复计算,把树形结构优化成线性结构,最终的算法复杂度一般是多项式级别。
上述两种优化只是减少了无效穷举,但依然都穷举了所有可行解,从而试图寻找最优的那个解。
贪心算法和它们的区别是:有些问题其实不需要完整地穷举所有可行解,就可以推导出最优解。这样一来,进一步减少了穷举空间,效率自然会更高。
举个简单的例子,就能直观的展现贪心算法了。
比方说现在有两种钞票,面额分为为 1 元和 100 元,每种钞票的数量无限,但现在你只能选择 10 张,请问你应该如何选择,才能使得总金额最大?
那你肯定会说,这还用问么?肯定是 10 张全拿 100 元的钞票,共计 1000 元,这就是最优策略,但凡犹豫一秒就是傻瓜。
你这么说,也对,也不对。
说你对,因为这确实是最优解法,没毛病。
说你不对,是因为这个解法暴露的是你只想捞钱的本质 (¬‿¬) ,跳过了算法的产生、优化过程,不符合计算机思维。
那计算机就要提问了,一切算法的本质是穷举,现在你还没有穷举出所有可能的解法,凭什么说这就是最优解呢?
按照算法思维,这个问题的本质是做 10 次选择,每次选择有两种可能,分别是 1 元和 100 元,一共有 种可能的选择。
所以你心里首先应该出现一棵高度为 10 的二叉树来穷举所有可行解,遍历这些可行解,然后可以得到最优解。
比如下面这个可视化面板画出了这棵递归树,不过因为做 10 次选择产生的二叉树太大,这里只画了做 3 次选择的情况:
算法可视化面板
心里只要有这样一棵二叉树,就应该能写出代码:
// 定义:做 n 次选择,返回可以获得的最大金额
int findMax(int n) {
if (n == 0) return 0;
// 这次选择 1 元,然后递归求解剩下的 n - 1 次选择的最大值
int result1 = 1 + findMax(n - 1);
// 这次选择 100 元,然后递归求解剩下的 n - 1 次选择的最大值
int result2 = 100 + findMax(n - 1);
// 返回两种选择中的最大值
return Math.max(result1, result2);
}这个算法的复杂度是二叉树的节点数量,是指数级别,非常高。不过到这里你应该已经看出来了,findMax(n - 1) 的值肯定都一样,那么 100 + findMax(n - 1) 必然大于 1 + findMax(n - 1),因此可以进行优化:
// 优化一、没必要对两种选择进行比较了
int findMax(int n) {
if (n == 0) return 0;
int result = 100 + findMax(n - 1);
return result;
}
// 优化二、递归改为迭代
int findMax(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += 100;
}
return result;
}
// 优化三、直接计算结果就行了
int findMax(int n) {
return 100 * n;
}这就是贪心算法,复杂度从 优化到了 ,堪称离谱。
你可能会认为这个例子太简单?
其实算法本来就很简单,就是穷举,有什么了不起的嘛?围绕穷举,衍生出各种优化方法,起了些花里胡哨的名字,不懂的人就容易被名字骗到,其实从原理上讲,没多大差别,不过是见招拆招罢了。
上面的例子虽然简单,但已经蕴含了贪心算法的精髓。接下来我们拓展延伸一下,在真实的算法问题中,如何发现贪心选择性质,如何用贪心算法解决问题。