贪心算法解题框架
一句话总结
回溯算法的剪枝优化是提前排除不可能的答案,使树结构尽可能小,最终的算法复杂度一般是指数级别;动态规划的备忘录优化是为了避免重复计算,把树形结构优化成线性结构,最终的算法复杂度一般是多项式级别。
上述两种优化只是减少了无效枚举,但依然都枚举了所有可行解,从而试图寻找最优的那个解。
贪心算法和它们的区别是:有些问题其实不需要完整地枚举所有可行解,就可以推导出最优解。这样一来,进一步减少了枚举空间,效率自然会更高。
举个简单的例子,就能直观地展现贪心算法了。
问题一
比方说现在有两种钞票,面额分别为 1 元和 100 元,每种钞票的数量无限,但现在你只能选择 10 张,请问你应该如何选择,才能使得总金额最大?
那你肯定会说,肯定是 10 张全拿 100 元的钞票,共计 1000 元,这就是最优策略。
但按照算法思维,这个问题的本质是做 10 次选择,每次选择有两种可能,分别是 1 元和 100 元,一共有 种可能的选择。
所以你心里首先应该出现一棵高度为 10 的二叉树来枚举所有可行解,遍历这些可行解,然后可以得到最优解。
心里只要有这样一棵二叉树,就应该能写出代码:
// 定义:做 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 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;
}
这就是贪心算法,复杂度从 优化到了 ,堪称离谱。
其实算法本来就很简单,就是枚举,是不是很简单? 围绕枚举,衍生出各种优化方法,起了些名字,其实从原理上讲,没多大差别,不过是见招拆招罢了。
贪心选择性质
首先来举例说明什么是