贪心算法完全解析

学习建议

贪心算法看似简单,但正确性证明是难点。建议结合具体例子理解,预计学习时间 1-2 周。

一、什么是贪心算法? 🎯

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

核心思想

贪心 vs 动态规划

特性 贪心算法 动态规划
选择策略 当前最优 考虑所有可能
回溯 不回溯 可能需要回溯
时间复杂度 通常更低 通常较高
适用条件 需满足贪心性质 需满足最优子结构
正确性 需要证明 自动保证
重要提醒

贪心算法并不总是能得到最优解!只有当问题满足贪心选择性质时,贪心才是正确的。

二、经典贪心问题 🏆

1. 活动选择问题

问题:给定 n 个活动的开始和结束时间,选择最多的互不冲突的活动。

活动选择问题
def activity_selection(activities):
    """
    活动选择问题 - 贪心算法
    :param activities: 活动列表 [(start, end, name), ...]
    :return: 选中的活动列表
    """
    # 按结束时间排序(贪心策略)
    activities.sort(key=lambda x: x[1])
    
    selected = []
    last_end_time = 0
    
    for start, end, name in activities:
        # 如果当前活动的开始时间 >= 上一个活动的结束时间
        if start >= last_end_time:
            selected.append((start, end, name))
            last_end_time = end
    
    return selected

# 测试
activities = [
    (1, 3, 'A'),
    (2, 5, 'B'),
    (3, 8, 'C'),
    (5, 7, 'D'),
    (6, 9, 'E'),
    (8, 10, 'F')
]

result = activity_selection(activities)
print("选中的活动:")
for start, end, name in result:
    print(f"{name}: {start}-{end}")

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

为什么正确?

2. 分数背包问题

问题:给定 n 个物品,每个物品有重量和价值,背包容量有限。可以取物品的一部分,求最大价值。

python
def fractional_knapsack(items, capacity):
    """
    分数背包问题 - 贪心算法
    :param items: 物品列表 [(weight, value), ...]
    :param capacity: 背包容量
    :return: 最大价值
    """
    # 计算单位重量价值,并按其降序排序
    items_with_ratio = [(w, v, v/w) for w, v in items]
    items_with_ratio.sort(key=lambda x: x[2], reverse=True)
    
    total_value = 0
    remaining_capacity = capacity
    
    for weight, value, ratio in items_with_ratio:
        if remaining_capacity <= 0:
            break
        
        # 如果能完全装入
        if weight <= remaining_capacity:
            total_value += value
            remaining_capacity -= weight
        else:
            # 只能装一部分
            fraction = remaining_capacity / weight
            total_value += value * fraction
            remaining_capacity = 0
    
    return total_value

# 测试
items = [(10, 60), (20, 100), (30, 120)]
capacity = 50
print(f"最大价值: {fractional_knapsack(items, capacity)}")  # 240.0

贪心策略:优先选择单位重量价值最高的物品

注意区分

3. 霍夫曼编码

霍夫曼编码是一种用于数据压缩的贪心算法。

霍夫曼编码简化版
import heapq
from collections import Counter

def huffman_encoding(text):
    """
    霍夫曼编码
    :param text: 输入文本
    :return: 编码字典和编码后的文本
    """
    # 统计频率
    freq = Counter(text)
    
    # 构建最小堆
    heap = [[weight, [char, ""]] for char, weight in freq.items()]
    heapq.heapify(heap)
    
    # 构建霍夫曼树
    while len(heap) > 1:
        lo = heapq.heappop(heap)
        hi = heapq.heappop(heap)
        
        for pair in lo[1:]:
            pair[1] = '0' + pair[1]
        for pair in hi[1:]:
            pair[1] = '1' + pair[1]
        
        heapq.heappush(heap, [lo[0] + hi[0]] + lo[1:] + hi[1:])
    
    # 生成编码字典
    huffman_code = sorted(heapq.heappop(heap)[1:], key=lambda p: (len(p[-1]), p))
    code_dict = {char: code for char, code in huffman_code}
    
    # 编码文本
    encoded_text = ''.join([code_dict[char] for char in text])
    
    return code_dict, encoded_text

# 测试
text = "this is an example of huffman encoding"
code_dict, encoded = huffman_encoding(text)
print("编码字典:", code_dict)
print("原始长度:", len(text) * 8, "bits")
print("编码长度:", len(encoded), "bits")

三、贪心算法的证明方法 📐

证明贪心算法的正确性是难点,常用方法有:

1. 贪心 stays ahead(保持领先)

证明贪心算法的每一步都不比任何其他算法差。

示例:活动选择问题

2. 交换论证(Exchange Argument)

假设存在一个最优解,通过交换操作将其转换为贪心解,且不降低解的质量。

交换论证步骤
1. 假设存在最优解 O 2. 找到 O 与贪心解 G 的第一个不同之处 3. 通过交换使 O 更接近 G 4. 证明交换后仍然是最优解 5. 重复直到 O = G

3. 数学归纳法

对问题规模进行归纳,证明贪心策略对所有规模都有效。

四、贪心的应用场景 💼

场景一:任务调度

操作系统使用贪心策略进行进程调度:

场景二:最小生成树

Kruskal 和 Prim 算法都是贪心算法:

Prim 算法(贪心)
import heapq

def prim_mst(graph, start):
    """
    Prim 最小生成树算法 - 贪心策略
    :param graph: 邻接表 {节点: [(邻居, 权重), ...]}
    :param start: 起始节点
    :return: MST 的边列表和总权重
    """
    mst_edges = []
    visited = set([start])
    total_weight = 0
    
    # 优先队列:(权重, 从节点, 到节点)
    edges = [(weight, start, neighbor) 
             for neighbor, weight in graph[start]]
    heapq.heapify(edges)
    
    while edges and len(visited) < len(graph):
        weight, u, v = heapq.heappop(edges)
        
        if v in visited:
            continue
        
        visited.add(v)
        mst_edges.append((u, v, weight))
        total_weight += weight
        
        # 添加新节点的边
        for neighbor, w in graph[v]:
            if neighbor not in visited:
                heapq.heappush(edges, (w, v, neighbor))
    
    return mst_edges, total_weight

# 测试
graph = {
    'A': [('B', 1), ('C', 4)],
    'B': [('A', 1), ('C', 2), ('D', 5)],
    'C': [('A', 4), ('B', 2), ('D', 1)],
    'D': [('B', 5), ('C', 1)]
}

edges, weight = prim_mst(graph, 'A')
print("MST 边:")
for u, v, w in edges:
    print(f"{u} - {v}: {w}")
print(f"总权重: {weight}")

场景三:数据压缩

场景四:网络路由

五、贪心的陷阱与误区 ⚠️

误区一:贪心总是最优

错误! 很多情况下贪心只能得到近似解。

反例:0-1 背包问题

  • 贪心策略:优先选价值高的
  • 结果:可能不是最优
  • 正确做法:动态规划
误区二:局部最优等于全局最优

局部最优选择的组合不一定导致全局最优。

反例:找零钱问题(某些币值系统)

  • 币值:1, 3, 4
  • 要找 6:贪心选 4+1+1(3个硬币)
  • 最优解:3+3(2个硬币)
陷阱:未验证贪心性质

在使用贪心前,必须证明问题满足贪心选择性质。

验证方法

  1. 尝试构造反例
  2. 使用交换论证
  3. 参考已知结论
技巧:先尝试贪心

对于优化问题,可以先尝试贪心算法:

  • 如果得到正确答案,再尝试证明
  • 如果不对,考虑动态规划或其他方法
  • 贪心可以作为 DP 的上界或启发式

六、贪心算法总结表 📊

O(n log n)
典型时间复杂度
O(n)
典型空间复杂度
需证明
正确性要求

常见贪心问题速查

活动选择

策略:按结束时间排序 证明:交换论证

分数背包

策略:按单位价值排序 证明:贪心 stays ahead

最小生成树

策略:选最小权重边 证明:cut property

最短路径

策略:选最近节点 证明:三角不等式

七、练习与挑战 🏆

推荐练习题

LeetCode 455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 难度:⭐ 简单 贪心策略:优先满足胃口小的孩子
LeetCode 122. 买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。 难度:⭐ 简单 贪心策略:只要今天比昨天高就卖出
LeetCode 55. 跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。 难度:⭐⭐ 中等 贪心策略:维护最远可达位置

八、学习路径建议 🗺️

第1-3天
基础理解
掌握贪心的基本概念,实现简单例子。
第4-7天
经典问题
深入学习活动选择、背包、霍夫曼编码等问题。
第8-10天
证明方法
学习如何证明贪心算法的正确性。
第11-14天
实战练习
完成至少 15 道 LeetCode 贪心题目。

九、总结 🎯

贪心算法是一种强大的优化工具,但需要谨慎使用。

掌握贪心思维

贪心算法虽然简单,但背后的思想深刻。继续练习,培养贪心直觉!

查看贪心专题练习 下载完整代码

© 2026 AI未来实验室 · 用 AI 探索无限可能
站点地图 | Robots.txt | 关于