【数据结构与算法】第48篇:算法思想(三):贪心算法

目录

一、什么是贪心算法

[1.1 贪心策略](#1.1 贪心策略)

[1.2 贪心 vs 动态规划](#1.2 贪心 vs 动态规划)

二、经典问题一:活动选择问题

[2.1 问题描述](#2.1 问题描述)

[2.2 贪心证明思路](#2.2 贪心证明思路)

[2.3 代码实现](#2.3 代码实现)

[2.4 复杂度分析](#2.4 复杂度分析)

三、经典问题二:找零问题

[3.1 问题描述](#3.1 问题描述)

[3.2 贪心的局限性](#3.2 贪心的局限性)

[3.3 代码实现(贪心)](#3.3 代码实现(贪心))

四、贪心算法的正确性条件

[4.1 贪心选择性质](#4.1 贪心选择性质)

[4.2 最优子结构](#4.2 最优子结构)

五、其他贪心算法经典问题

六、贪心算法解题模板

[七、贪心 vs DP 选择指南](#七、贪心 vs DP 选择指南)

八、小结

九、思考题


一、什么是贪心算法

1.1 贪心策略

贪心算法在每一步都选择当前状态下的最优解(局部最优),而不考虑未来的影响。

特点

  • 实现简单,通常很快

  • 不能保证对所有问题都得到全局最优解

  • 需要证明问题具有贪心选择性质

1.2 贪心 vs 动态规划

对比项 贪心算法 动态规划
决策方式 单次决策,不回头 考虑所有可能性
子问题重叠 不一定 必然
典型问题 活动选择、找零 背包、LCS
时间复杂度 通常 O(n) 或 O(n log n) 通常 O(n²) 或更高
全局最优保证 需要证明 总是保证

二、经典问题一:活动选择问题

2.1 问题描述

有 n 个活动,每个活动有开始时间 s[i] 和结束时间 f[i]。活动不能同时进行(时间不能重叠)。求最多能安排多少个活动。

贪心策略:每次选择结束时间最早且与已选活动不冲突的活动。

2.2 贪心证明思路

为什么选结束时间最早的是最优?

  • 选结束时间最早的活动,能为剩余活动留出最多的时间

  • 这个选择不会比任何最优解差

2.3 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>

// 活动结构体
typedef struct {
    int start;
    int finish;
} Activity;

// 按结束时间排序(升序)
int cmp(const void *a, const void *b) {
    return ((Activity*)a)->finish - ((Activity*)b)->finish;
}

// 贪心算法选择活动
int activitySelection(Activity activities[], int n) {
    if (n == 0) return 0;
    
    // 1. 按结束时间排序
    qsort(activities, n, sizeof(Activity), cmp);
    
    // 2. 贪心选择
    int count = 1;
    int lastFinish = activities[0].finish;
    
    printf("选中的活动: (0, %d)\n", activities[0].finish);
    
    for (int i = 1; i < n; i++) {
        if (activities[i].start >= lastFinish) {
            printf("选中的活动: (%d, %d)\n", activities[i].start, activities[i].finish);
            count++;
            lastFinish = activities[i].finish;
        }
    }
    
    return count;
}

int main() {
    Activity activities[] = {
        {1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 9},
        {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 14}, {12, 16}
    };
    int n = sizeof(activities) / sizeof(activities[0]);
    
    int result = activitySelection(activities, n);
    printf("最多可安排活动数: %d\n", result);
    
    return 0;
}

运行结果:

text

复制代码
选中的活动: (0, 4)
选中的活动: (5, 7)
选中的活动: (8, 11)
选中的活动: (12, 16)
最多可安排活动数: 4

2.4 复杂度分析

  • 排序:O(n log n)

  • 贪心选择:O(n)

  • 总时间复杂度:O(n log n)


三、经典问题二:找零问题

3.1 问题描述

给定面额数组 coins(假设无限供应)和金额 amount,求最少需要多少枚硬币。

贪心策略:每次用最大面额不超过剩余金额的硬币。

3.2 贪心的局限性

贪心算法不一定对所有硬币系统都正确。

硬币系统 金额 贪心结果 最优结果 是否正确
1,5,10,25(标准) 30 25+5 → 2枚 10+10+10 → 3枚
1,3,4 6 4+1+1 → 3枚 3+3 → 2枚

3.3 代码实现(贪心)

c

复制代码
#include <stdio.h>
#include <stdlib.h>

// 按面额降序排序
int cmpDesc(const void *a, const void *b) {
    return *(int*)b - *(int*)a;
}

// 贪心找零
void greedyCoinChange(int coins[], int n, int amount) {
    qsort(coins, n, sizeof(int), cmpDesc);
    
    printf("贪心找零 %d 元: ", amount);
    int remaining = amount;
    
    for (int i = 0; i < n && remaining > 0; i++) {
        if (remaining >= coins[i]) {
            int count = remaining / coins[i];
            printf("%d枚%d元 ", count, coins[i]);
            remaining -= count * coins[i];
        }
    }
    
    if (remaining > 0) {
        printf("无法找零");
    }
    printf("\n");
}

int main() {
    int coins1[] = {1, 5, 10, 25};
    int coins2[] = {1, 3, 4};
    
    greedyCoinChange(coins1, 4, 30);
    greedyCoinChange(coins2, 3, 6);
    
    return 0;
}

运行结果:

text

复制代码
贪心找零 30 元: 1枚25元 1枚5元 
贪心找零 6 元: 1枚4元 2枚1元 

四、贪心算法的正确性条件

4.1 贪心选择性质

定义:问题的全局最优解可以通过一系列局部最优选择(贪心选择)得到。

证明方法

  • 假设有一个最优解

  • 证明可以将其改造为包含贪心选择的解

  • 用数学归纳法或反证法

4.2 最优子结构

定义:问题的最优解包含子问题的最优解。

活动选择问题满足最优子结构:去掉贪心选择的活动后,剩余子问题的最优解加上这个活动就是全局最优解。


五、其他贪心算法经典问题

问题 贪心策略 时间复杂度
哈夫曼编码 每次合并频率最小的两个节点 O(n log n)
最小生成树(Prim/Kruskal) 每次选最小权边 O(E log V)
最短路径(Dijkstra) 每次选距离最小的顶点 O(V²) 或 O(E log V)
区间覆盖 每次选能覆盖最远右端点的区间 O(n log n)
任务调度(最小化总完成时间) 按处理时间升序 O(n log n)

六、贪心算法解题模板

c

复制代码
// 贪心算法通用框架
void greedy() {
    // 1. 按某种规则排序(如果需要)
    sort(data);
    
    // 2. 初始化结果
    result = initial;
    
    // 3. 贪心选择
    for (each element in sorted order) {
        if (符合条件) {
            选择当前元素;
            更新状态;
        }
    }
}

七、贪心 vs DP 选择指南

问题特征 推荐算法
可以证明贪心选择性质 贪心(更高效)
需要考虑多种可能性 DP
子问题重叠明显 DP
只需局部最优决策 贪心
可分解为独立子问题 分治或贪心

判断方法

  1. 尝试贪心策略,看能否举出反例

  2. 如果不能,考虑DP

  3. 如果DP太慢,尝试证明贪心正确


八、小结

这一篇我们学习了贪心算法:

要点 说明
核心思想 每次选当前最优,希望达到全局最优
活动选择 选结束时间最早的,复杂度 O(n log n)
找零问题 用最大面额优先,但不一定正确
正确性条件 贪心选择性质 + 最优子结构
适用范围 图论(MST、最短路)、调度、哈夫曼编码

贪心 vs DP

  • 贪心:局部最优 → 全局最优(需要证明)

  • DP:考虑所有可能性 → 全局最优(保证正确)

学习建议

  • 多练习判断问题是否适合贪心

  • 学会举反例推翻错误贪心策略

  • 掌握常见贪心问题(活动选择、哈夫曼、区间覆盖)

下一篇我们讲代码调试技巧与常见内存错误排查。


九、思考题

  1. 活动选择问题中,如果改成选开始时间最早或持续时间最短,能得到最优解吗?为什么?

  2. 硬币系统 {1, 5, 10, 25} 中贪心总是正确的,但 {1, 3, 4} 中不对。硬币系统满足什么条件时贪心正确?

  3. 如何用贪心算法解决"用最少的箭射爆所有气球"问题?

  4. 如果活动有时间权重(每个活动有不同价值),如何用DP解决?

欢迎在评论区讨论你的答案。

相关推荐
csbysj20203 小时前
Java 正则表达式
开发语言
_深海凉_3 小时前
LeetCode热题100-爬楼梯
算法·leetcode·职场和发展
小明的IT世界3 小时前
编程智能体为何能让LLM在实际工作中表现更好
java·开发语言·人工智能·ai编程
j_xxx404_3 小时前
力扣C++算法:哈希表(存在重复元素|存在重复元素II|字母异位词分组)
算法·leetcode·散列表
穿条秋裤到处跑3 小时前
每日一道leetcode(2026.04.17):镜像对之间最小绝对距离
算法·leetcode
codebrick3 小时前
408 数据结构:快排 / 堆排 / 归并 / 希尔 等排序算法对比(复杂度、稳定性、真题考点
数据结构·考研·算法·排序算法·408
脱氧核糖核酸__3 小时前
LeetCode热题100——240.搜索二维矩阵 II(题目+题解+答案)
c++·算法·leetcode·矩阵
.柒宇.3 小时前
Java八股之== 与 equals 区别
java·开发语言
浪客川3 小时前
【百例RUST - 011】简单键值对
开发语言·后端·rust