数据结构与算法——贪心算法

引言

贪心算法(Greedy Algorithm),又称贪婪算法,是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。这种算法并不保证在所有情况下都能找到全局最优解,但在许多实际问题中,它能够以较高的效率得到相当不错的解,甚至是最优解。以下是对贪心算法的详细介绍:

基本思想

贪心算法的基本思想可以概括为“每一步都做出一个局部最优的选择,最终期望得到全局最优解”。它并不从整体最优的角度去考虑问题,而是通过在每个阶段选择当前状态下的最优解,来逐步逼近全局最优解。这种策略的关键在于贪心选择性质,即每一步的选择都是基于当前状态的最优解,而不考虑未来的状态或整体情况。

基本步骤

  1. 建立数学模型
    • 根据问题的具体情况,建立数学模型,明确问题的目标函数和约束条件。
  2. 贪心策略的选择
    • 根据问题的特性和要求,选择一个合适的贪心策略。这个策略应该能够指导算法在每一步做出最优的选择。
  3. 贪心算法的实现
    • 按照贪心策略,逐步求解问题。在每一步中,都选择当前状态下的最优解,并更新问题的状态。
  4. 解的有效性检查
    • 在算法结束时,检查得到的解是否满足问题的约束条件和目标要求。如果满足,则认为算法成功找到了问题的解;如果不满足,则需要重新考虑贪心策略的选择或采用其他算法求解。

优缺点

优点

  • 算法简单:贪心算法的实现通常比较直观和简单,易于理解和实现。
  • 效率高:由于贪心算法在每一步都选择最优解,因此它的执行效率通常比较高。
  • 适用范围广:贪心算法在解决一些特定类型的问题时,如数据压缩、最优装载、活动选择等,能够取得很好的效果。

缺点

  • 无法保证全局最优:贪心算法并不总是能够得到全局最优解。在某些情况下,它可能会陷入局部最优解而无法跳出。
  • 贪心策略的选择依赖性强:贪心算法的效果很大程度上取决于贪心策略的选择。如果贪心策略选择不当,可能会导致算法无法得到满意的解。
  • 正确性证明困难:对于某些问题,要证明贪心算法的正确性(即能够得到全局最优解)可能比较困难。这通常需要较深的数学功底和逻辑推理能力。

应用领域

贪心算法在解决许多实际问题时都有广泛的应用,包括但不限于以下几个方面:

  • 数据压缩:如哈夫曼编码,通过贪心策略选择出现频率最高的字符进行编码,以达到压缩数据的目的。
  • 最优装载问题:在装载货物时,通过贪心策略选择当前能够装载的最大货物,以达到最优装载效果。
  • 最小生成树:Prim算法和Kruskal算法都是基于贪心策略的求解最小生成树的算法。
  • 活动选择问题:在安排活动时,通过贪心策略选择结束时间最早的活动,以使得尽可能多的活动能够被安排。
  • 多机调度问题:在安排任务到多台机器上时,通过贪心策略选择当前能够最快完成的任务进行分配,以提高整体效率。

总之,贪心算法是一种简单而有效的算法策略,在解决许多实际问题时都能够取得不错的效果。然而,它也存在一些局限性,如无法保证全局最优解等。因此,在使用贪心算法时需要根据问题的具体情况进行选择和调整。

经典例题

题目一:分发饼干(LeetCode 455)

题目描述
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸s[j]。如果s[j] >= g[i],我们可以将这个饼干j分配给孩子i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

解题思路

  1. 将孩子的胃口值和饼干尺寸分别进行排序。
  2. 使用两个指针分别指向孩子和饼干数组的起始位置。
  3. 遍历排序后的数组,对于每个孩子,从当前饼干指针开始向后查找,找到第一个满足孩子胃口的饼干。
  4. 如果找到满足的饼干,将孩子指针和饼干指针都向后移动一位;否则,只将饼干指针向后移动一位。
  5. 重复步骤3和4,直到遍历完所有孩子或饼干。
  6. 返回孩子指针的位置,即满足的孩子数量。

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。

解题思路

  1. 使用三个变量分别记录5美元、10美元和20美元钞票的数量。
  2. 遍历账单列表,对于每张账单:
    • 如果账单是5美元,则增加5美元钞票的数量。
    • 如果账单是10美元,则尝试使用一张5美元钞票找零,如果5美元钞票不足,则返回false;否则,减少5美元钞票的数量,并增加10美元钞票的数量。
    • 如果账单是20美元,则优先尝试使用一张10美元和一张5美元钞票找零;如果10美元钞票不足,但5美元钞票足够两张,则使用两张5美元钞票找零;如果两者都不足,则返回false;否则,进行相应的钞票数量调整。
  3. 如果所有顾客都得到了正确的找零,则返回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;  
    }  
};
  • 15
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值