引言
贪心算法(Greedy Algorithm),又称贪婪算法,是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。这种算法并不保证在所有情况下都能找到全局最优解,但在许多实际问题中,它能够以较高的效率得到相当不错的解,甚至是最优解。以下是对贪心算法的详细介绍:
基本思想
贪心算法的基本思想可以概括为“每一步都做出一个局部最优的选择,最终期望得到全局最优解”。它并不从整体最优的角度去考虑问题,而是通过在每个阶段选择当前状态下的最优解,来逐步逼近全局最优解。这种策略的关键在于贪心选择性质,即每一步的选择都是基于当前状态的最优解,而不考虑未来的状态或整体情况。
基本步骤
- 建立数学模型:
- 根据问题的具体情况,建立数学模型,明确问题的目标函数和约束条件。
- 贪心策略的选择:
- 根据问题的特性和要求,选择一个合适的贪心策略。这个策略应该能够指导算法在每一步做出最优的选择。
- 贪心算法的实现:
- 按照贪心策略,逐步求解问题。在每一步中,都选择当前状态下的最优解,并更新问题的状态。
- 解的有效性检查:
- 在算法结束时,检查得到的解是否满足问题的约束条件和目标要求。如果满足,则认为算法成功找到了问题的解;如果不满足,则需要重新考虑贪心策略的选择或采用其他算法求解。
优缺点
优点:
- 算法简单:贪心算法的实现通常比较直观和简单,易于理解和实现。
- 效率高:由于贪心算法在每一步都选择最优解,因此它的执行效率通常比较高。
- 适用范围广:贪心算法在解决一些特定类型的问题时,如数据压缩、最优装载、活动选择等,能够取得很好的效果。
缺点:
- 无法保证全局最优:贪心算法并不总是能够得到全局最优解。在某些情况下,它可能会陷入局部最优解而无法跳出。
- 贪心策略的选择依赖性强:贪心算法的效果很大程度上取决于贪心策略的选择。如果贪心策略选择不当,可能会导致算法无法得到满意的解。
- 正确性证明困难:对于某些问题,要证明贪心算法的正确性(即能够得到全局最优解)可能比较困难。这通常需要较深的数学功底和逻辑推理能力。
应用领域
贪心算法在解决许多实际问题时都有广泛的应用,包括但不限于以下几个方面:
- 数据压缩:如哈夫曼编码,通过贪心策略选择出现频率最高的字符进行编码,以达到压缩数据的目的。
- 最优装载问题:在装载货物时,通过贪心策略选择当前能够装载的最大货物,以达到最优装载效果。
- 最小生成树:Prim算法和Kruskal算法都是基于贪心策略的求解最小生成树的算法。
- 活动选择问题:在安排活动时,通过贪心策略选择结束时间最早的活动,以使得尽可能多的活动能够被安排。
- 多机调度问题:在安排任务到多台机器上时,通过贪心策略选择当前能够最快完成的任务进行分配,以提高整体效率。
总之,贪心算法是一种简单而有效的算法策略,在解决许多实际问题时都能够取得不错的效果。然而,它也存在一些局限性,如无法保证全局最优解等。因此,在使用贪心算法时需要根据问题的具体情况进行选择和调整。
经典例题
题目一:分发饼干(LeetCode 455)
题目描述:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸s[j]。如果s[j] >= g[i],我们可以将这个饼干j分配给孩子i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
解题思路:
- 将孩子的胃口值和饼干尺寸分别进行排序。
- 使用两个指针分别指向孩子和饼干数组的起始位置。
- 遍历排序后的数组,对于每个孩子,从当前饼干指针开始向后查找,找到第一个满足孩子胃口的饼干。
- 如果找到满足的饼干,将孩子指针和饼干指针都向后移动一位;否则,只将饼干指针向后移动一位。
- 重复步骤3和4,直到遍历完所有孩子或饼干。
- 返回孩子指针的位置,即满足的孩子数量。
C++代码示例:
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int child = 0, cookie = 0;
while (child < g.size() && cookie < s.size()) {
if (g[child] <= s[cookie]) {
child++;
}
cookie++;
}
return child;
}
};
题目二:柠檬水找零(LeetCode 860)
题目描述:
在柠檬水摊上,每一杯柠檬水的售价为5美元。顾客排队购买你的产品(按账单支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付5美元、10美元或20美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付5美元。注意,一开始你手头没有任何零钱。如果你能给每位顾客正确找零,返回true;否则返回false。
解题思路:
- 使用三个变量分别记录5美元、10美元和20美元钞票的数量。
- 遍历账单列表,对于每张账单:
- 如果账单是5美元,则增加5美元钞票的数量。
- 如果账单是10美元,则尝试使用一张5美元钞票找零,如果5美元钞票不足,则返回false;否则,减少5美元钞票的数量,并增加10美元钞票的数量。
- 如果账单是20美元,则优先尝试使用一张10美元和一张5美元钞票找零;如果10美元钞票不足,但5美元钞票足够两张,则使用两张5美元钞票找零;如果两者都不足,则返回false;否则,进行相应的钞票数量调整。
- 如果所有顾客都得到了正确的找零,则返回true。
C++代码示例:
#include <vector>
using namespace std;
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) return false;
five--;
ten++;
} else if (bill == 20) {
if (ten > 0 && five > 0) {
ten--;
five--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
};