C++ 贪心算法入门教程(0基础友好 | 通俗易懂 | 样式丰富)


🌟什么是贪心算法?

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优的选择,希望通过局部最优解达到全局最优解的算法策略。

注意: 贪心算法并不总是能得到全局最优解,但在某些特定问题中非常有效!


🧠贪心算法的核心思想

  • 每一步都做出一个看起来最优的选择
  • 不考虑整体结构,只关注当前状态下的“眼前利益”
  • 一旦做出选择就不能回退

🎯适用场景(常见问题)

贪心算法适用于以下类型的问题:

  1. 找零钱问题
  2. 活动选择问题
  3. 区间调度问题
  4. 霍夫曼编码(压缩)
  5. 最小生成树中的 Prim 和 Kruskal 算法(使用贪心思想)

🚀从最简单的例子开始:找零钱问题

💡问题描述:

你有若干种面值的硬币(如 1 元、2 元、5 元),现在要凑出某个金额 n,要求使用最少数量的硬币。

✅贪心思路:

每次选择最大可用的硬币面值。

#include <iostream>
using namespace std;

int main() {
    int coins[] = {5, 2, 1}; // 面值从大到小排列
    int n = 17;              // 要凑的金额
    int count = 0;           // 记录使用的硬币数

    for (int i = 0; i < 3; i++) {
        if (n >= coins[i]) {
            int num = n / coins[i];  // 最多能用几个这个面值的硬币
            count += num;
            n -= num * coins[i];     // 减去已使用的金额
        }
    }

    cout << "最少需要 " << count << " 枚硬币。" << endl;
    return 0;
}

📌 输出:

最少需要 4 枚硬币。

即 5×3 + 2×1 = 17,共 4 枚。


📌贪心算法的一般步骤总结:

  1. 确定问题是否适合用贪心解决
  2. 设计贪心策略
  3. 对数据进行排序或预处理
  4. 循环遍历,每次都做当前最优选择
  5. 记录结果并输出

🧩实战练习一:活动选择问题(Activity Selection)

💡问题描述:

给定多个活动的时间区间,从中选出最多可以互不重叠进行的活动个数。

✅贪心策略:

按结束时间从小到大排序,依次选择最早结束的活动。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Activity {
    int start, end;
};

// 按照结束时间升序排序
bool compare(Activity a, Activity b) {
    return a.end < b.end;
}

int main() {
    vector<Activity> activities = {
        {1, 4}, {3, 5}, {0, 6},
        {5, 7}, {3, 8}, {5, 9},
        {6, 10}, {8, 11}
    };

    sort(activities.begin(), activities.end(), compare);

    int count = 1;             // 第一个活动一定选
    int last_end = activities[0].end;

    for (int i = 1; i < activities.size(); i++) {
        if (activities[i].start >= last_end) {
            count++;
            last_end = activities[i].end;
        }
    }

    cout << "最多可以选择 " << count << " 个互不重叠的活动。" << endl;
    return 0;
}

📌 输出:

最多可以选择 4 个互不重叠的活动。

🧩实战练习二:背包问题(分数背包)

💡问题描述:

有一个容量为 W 的背包和若干物品,每个物品有价值和重量,可以取一部分装入背包,求最大总价值。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Item {
    int value, weight;
    double ratio; // 价值/重量比
};

// 按照价值密度从高到低排序
bool compare(Item a, Item b) {
    return a.ratio > b.ratio;
}

int main() {
    vector<Item> items = {
        {60, 10}, {100, 20}, {120, 30}
    };
    int W = 50;       // 背包容量
    double total = 0; // 总价值

    // 计算价值密度
    for (auto &item : items) {
        item.ratio = (double)item.value / item.weight;
    }

    sort(items.begin(), items.end(), compare);

    for (auto &item : items) {
        if (W == 0) break;
        if (item.weight <= W) {
            total += item.value;
            W -= item.weight;
        } else {
            total += item.ratio * W;
            W = 0;
        }
    }

    cout.precision(2);
    cout << fixed << "最大总价值为:" << total << endl;
    return 0;
}

📌 输出:

最大总价值为:240.00

📚贪心与动态规划的区别

特点 贪心算法 动态规划
是否回溯
时间复杂度 通常较低 通常较高
是否保证最优解 不一定 一般可以
使用场景 局部最优可导出全局最优 多阶段决策问题

🧠如何判断一个问题能否用贪心?

  • 具有贪心选择性质:可以通过一系列局部最优解得到全局最优解
  • 具有最优子结构性质:原问题的最优解包含子问题的最优解

🧑‍💻练习建议(适合新手)

  1. 完全理解上面两个例题(找零钱、活动选择)
  2. 尝试自己实现“会议安排”、“任务调度”等类似问题
  3. 学会使用 sort() 自定义排序规则
  4. 练习用浮点数计算性价比(如分数背包)
  5. 多画图模拟贪心过程

📚推荐学习路径

  1. 入门:掌握基本贪心问题(找零钱、活动选择)
  2. 提升:尝试解决更复杂的贪心题目(如:加油站问题、跳跃游戏)
  3. 拓展:结合其他算法(如排序+贪心、优先队列+贪心)

🧾结语

贪心算法是算法学习中的重要基石之一。它简单、高效,但也有局限性。希望你能通过本教程理解它的基本思想,并动手实践,真正掌握这一实用技能!


📖参考资料

  • 《算法导论》(Introduction to Algorithms)
  • LeetCode 上的经典贪心题目(如 455. 分发饼干、435. 无重叠区间)

1 条评论

  • @ 2025-5-13 20:39:08

    C++ 贪心算法入门教程

    贪心算法是一种在每一步选择中都采取当前状态下最优(局部最优)选择,从而希望导致全局结果也最优的算法策略。虽然贪心算法不能解决所有问题,但它可以高效解决许多经典问题。

    1. 贪心算法基本思想

    贪心算法的核心思想是:每一步都做出当前看起来最好的选择,而不考虑这种选择对未来的影响。这种策略在某些问题中可以直接得到全局最优解,但在其他问题中可能只能得到近似解。

    2. 贪心算法适用条件

    贪心算法适用于满足以下两个条件的问题:

    • 贪心选择性质:问题的全局最优解可以通过一系列局部最优选择得到。
    • 最优子结构性质:问题的最优解包含其子问题的最优解。

    3. 简单示例:找零问题

    问题描述:假设你是一个收银员,需要找给顾客一定金额的零钱,如何使用最少的硬币?

    示例代码

    #include <iostream>
    #include <vector>
    using namespace std; // 使用标准命名空间,避免重复写std::
    
    // 找零问题的贪心算法实现
    vector<int> greedyChange(int amount) {
        vector<int> coins = {25, 10, 5, 1}; // 硬币面额:25美分(quarter)、10美分(dime)、5美分(nickel)、1美分(penny)
        vector<int> result(coins.size(), 0); // 每种硬币的数量
        
        for (int i = 0; i < coins.size(); i++) {
            // 尽可能多地使用当前最大面额的硬币
            result[i] = amount / coins[i];
            amount = amount % coins[i]; // 剩余需要找零的金额
        }
        
        return result;
    }
    
    int main() {
        int amount = 63; // 需要找零63美分
        vector<int> change = greedyChange(amount);
        
        cout << "找零 " << amount << " 美分需要的硬币数量:" << endl;
        vector<int> coins = {25, 10, 5, 1};
        
        for (int i = 0; i < coins.size(); i++) {
            if (change[i] > 0) {
                cout << coins[i] << "美分: " << change[i] << "个" << endl;
            }
        }
        
        return 0;
    }
    

    代码解释

    • 硬币面额数组 coins 包含四种美国硬币面额(25、10、5、1美分)。
    • 贪心策略:每次都选择当前可用的最大面额硬币,直到剩余金额为0。
    • 结果验证:对于63美分的找零,算法会返回2个25美分、1个10美分和3个1美分,共6个硬币,这是最优解。

    4. 进阶示例:活动选择问题

    问题描述:有多个活动需要使用同一资源(如会议室),每个活动有开始时间和结束时间,如何选择最多的活动,使得它们互不冲突?

    示例代码

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std; // 使用标准命名空间,避免重复写std::
    
    // 活动结构体,包含活动编号、开始时间和结束时间
    struct Activity {
        int id;
        int start;
        int end;
    };
    
    // 比较函数,按结束时间升序排序
    bool compareActivities(const Activity& a, const Activity& b) {
        return a.end < b.end;
    }
    
    // 活动选择的贪心算法实现
    vector<int> activitySelection(vector<Activity>& activities) {
        // 按结束时间排序
        sort(activities.begin(), activities.end(), compareActivities);
        
        vector<int> selected;
        if (activities.empty()) return selected;
        
        // 选择第一个结束的活动
        selected.push_back(activities[0].id);
        int lastEndTime = activities[0].end;
        
        // 遍历剩余活动,选择开始时间晚于上一个结束时间的活动
        for (int i = 1; i < activities.size(); i++) {
            if (activities[i].start >= lastEndTime) {
                selected.push_back(activities[i].id);
                lastEndTime = activities[i].end;
            }
        }
        
        return selected;
    }
    
    int main() {
        // 示例活动:(编号, 开始时间, 结束时间)
        vector<Activity> activities = {
            {1, 1, 4}, {2, 3, 5}, {3, 0, 6}, {4, 5, 7},
            {5, 3, 9}, {6, 5, 9}, {7, 6, 10}, {8, 8, 11},
            {9, 8, 12}, {10, 2, 14}, {11, 12, 16}
        };
        
        vector<int> selected = activitySelection(activities);
        
        cout << "最多可以选择的活动数量:" << selected.size() << endl;
        cout << "选择的活动编号为:";
        for (int id : selected) {
            cout << id << " ";
        }
        cout << endl;
        
        return 0;
    }
    

    代码解释

    • 活动结构体 Activity 存储每个活动的编号、开始时间和结束时间。
    • 贪心策略:每次选择结束时间最早且与已选活动不冲突的活动。
    • 排序操作:首先按结束时间对活动进行排序,确保每次能选择最早结束的活动。

    5. 贪心算法的优缺点

    优点

    • 算法简单,易于理解和实现。
    • 时间复杂度通常较低,效率高。

    缺点

    • 不能解决所有问题,只适用于满足贪心选择性质的问题。
    • 有时得到的解只是近似最优解,而非全局最优解。

    6. 练习题

    1. 分数背包问题:给定一组物品,每个物品有重量和价值,以及一个容量为C的背包,如何选择物品放入背包,使得背包中物品的总价值最大?注意:物品可以分割。

    2. 最小等待时间问题:有n个任务需要处理,每个任务需要一定的时间,如何安排处理顺序,使得所有任务的平均等待时间最短?

    3. 跳跃游戏:给定一个非负整数数组,每个元素代表在该位置可以跳跃的最大长度,判断是否可以跳到数组的最后一个位置。

    通过这些示例和练习,你应该对贪心算法有了基本的理解。贪心算法虽然简单,但在合适的问题中可以发挥很大作用。在实际应用中,需要仔细分析问题是否满足贪心选择性质,避免盲目使用贪心策略导致错误结果。

    • 1