news 2026/5/27 1:11:26

Python贪心算法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python贪心算法

一、贪心算法核心思想

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优或最有利的选择,从而希望导致结果是全局最优的算法策略。

贪心算法的基本特征:

  • 局部最优选择:每一步都选择当前看起来最好的选项

  • 不可回溯性:一旦做出选择,就不再重新考虑

  • 高效性:通常时间复杂度较低,实现简单

二、贪心算法的优缺点分析

优点:

  1. 时间复杂度低:通常是O(n log n)或O(n)

  2. 实现简单直观:代码通常简短易懂

  3. 空间效率高:多数情况下只需要常数额外空间

  4. 实时性好:适合在线处理和实时系统

缺点:

  1. 不保证全局最优:局部最优不一定导致全局最优

  2. 适用范围有限:只适用于具有贪心选择性质的问题

  3. 证明困难:需要严谨的数学证明

  4. 对输入敏感:不同输入可能需要不同的贪心策略

三、贪心算法的证明方法

方法一:反证法 - 例1:部分背包问题

反证法证明思路:
1、假设存在最优解A与贪心解B不同
2、找到第一个选择不同的物品,证明贪心选择不会更差
3、逐步调整,最终证明贪心解B至少与A一样好

问题:n个物品,重量w和价值v,背包容量C,可分割物品

# 读取物品数量n和背包容量t n, t = map(int, input().split()) # 存储物品信息:每个物品是(质量, 价值, 单位价值) golds = [] for i in range(n): m, v = map(int, input().split()) # 读取物品的质量和价值 p = v / m # 计算单位价值(价值/质量比) golds.append((m, v, p)) # 将物品信息添加到列表 # 按照单位价值从高到低排序(贪心策略:优先选择性价比高的物品) golds.sort(key=lambda x: x[2], reverse=True) # 初始化总价值和剩余容量 total_value = 0.0 remaining = t # 背包剩余容量 # 遍历排序后的物品 for m, v, p in golds: if remaining <= 0: # 如果背包已满,结束 break if m <= remaining: # 如果当前物品可以完整放入背包 total_value += v # 获得完整价值 remaining -= m # 减少剩余容量 else: # 如果只能放入部分 total_value += p * remaining # 按比例获得部分价值 remaining = 0 # 背包容量用完 # 输出结果,保留两位小数 print(f'{total_value:.2f}')

方法二:归纳法 - 例2:活动选择问题(凌乱的yyy)

归纳法证明:
1、基础:只有一个活动时,贪心选择最优
2、归纳:假设前k个活动的选择是最优的
3、递推:第k+1个活动选择结束最早的,证明仍然最优

问题:选择最大数量的互不重叠活动

# 读取活动数量 n = int(input()) # 存储活动信息:每个活动是(开始时间, 结束时间) contest = [] for i in range(n): a, b = map(int, input().split()) # 读取活动的开始时间和结束时间 contest.append((a, b)) # 将活动添加到列表 # 按照结束时间升序排序 # 贪心策略:优先选择结束时间早的活动,给后面的活动留下更多时间 contest.sort(key=lambda x: x[1]) # 初始化计数和上一个选择的活动的结束时间 count = 0 # 最多能选择的活动数量 last_end = -1 # 上一个选择的活动的结束时间,初始化为-1确保第一个活动可以被选择 # 遍历排序后的活动 for start, end in contest: # 如果当前活动的开始时间不早于上一个选择的活动的结束时间 # 说明两个活动不冲突,可以选择当前活动 if start >= last_end: count += 1 # 选择当前活动 last_end = end # 更新上一个选择的活动的结束时间 # 输出最多能选择的活动数量 print(count)

方法三:交换论证法 - 例3:排队接水问题

交换论证证明:
假设最优解中有两个相邻的人i,j,且t_i > t_j,交换i和j的位置,总等待时间不会增加,因此升序排列是最优的。

问题:n个人排队接水,如何安排使总等待时间最小

# 读取排队人数 n = int(input()) # 读取每个人的接水时间 time = list(map(int, input().split())) # 创建(编号, 时间)的元组列表 times = [] for i in range(n): t = time[i] times.append((i + 1, t)) # i+1作为编号(从1开始) # 按照接水时间升序排序,如果时间相同则按编号升序排序 # 贪心策略:让接水时间短的人先接水,可以减少总等待时间 times.sort(key=lambda x: (x[1], x[0])) # 输出接水顺序(编号) for i, t in times: print(i, end=' ') print() # 换行 # 计算总等待时间 waits = 0.0 # 使用浮点数以确保除法精度 for i in range(n): wait = 0 # 第i个人的等待时间 # 第i个人需要等待前面所有人的接水时间 for j in range(i): wait += times[j][1] waits += wait # 累加到总等待时间 # 计算平均等待时间 waits_mean = waits / n # 输出结果,保留两位小数 print(f'{waits_mean:.2f}')

四、经典贪心问题详解

例4:分卷子问题(最小合并代价)

问题:合并多堆卷子,每次合并两堆,代价为两堆之和,求最小总代价

import heapq def min_merge_cost_greedy(volumes): """ 分卷子问题贪心解法(哈夫曼树应用) 贪心策略:总是合并最小的两堆 参数: volumes: 各堆卷子的数量列表 返回: total_cost: 最小合并总代价 merge_sequence: 合并顺序记录 """ if len(volumes) <= 1: return 0, [] # 使用最小堆 min_heap = volumes[:] heapq.heapify(min_heap) total_cost = 0 merge_sequence = [] # 贪心合并:每次合并最小的两堆 while len(min_heap) > 1: # 取出最小的两个元素 first = heapq.heappop(min_heap) second = heapq.heappop(min_heap) # 合并代价 merge_cost = first + second total_cost += merge_cost merge_sequence.append((first, second, merge_cost)) # 将合并结果放回堆中 heapq.heappush(min_heap, merge_cost) return total_cost, merge_sequence # 示例使用 volumes = [3, 4, 2, 6, 1] min_cost, sequence = min_merge_cost_greedy(volumes) print(f"最小合并代价: {min_cost}") print(f"合并顺序: {sequence}")

例5:合并果子问题

问题:类似分卷子,但可能有额外约束

import heapq # 导入堆队列算法模块(最小堆) # 读取水果数量 n = int(input()) # 读取每个水果的重量,转换为列表 fruits = list(map(int, input().split())) # 将列表转换为最小堆(堆化) # 堆是一种特殊的二叉树结构,最小堆的根节点总是最小的元素 heapq.heapify(fruits) # 初始化总消耗体力值 total_cost = 0 # 当堆中还有不止一个水果时继续合并 while len(fruits) > 1: # 取出堆中最小的两个水果(堆顶元素) a = heapq.heappop(fruits) # 弹出并返回最小元素 b = heapq.heappop(fruits) # 弹出并返回次小元素 # 合并这两个水果需要的体力值 = 它们的重量之和 cost = a + b # 累加到总消耗体力值 total_cost += cost # 将合并后的新水果(重量为cost)放回堆中 heapq.heappush(fruits, cost) # 将新元素推入堆,保持堆性质 # 输出合并所有水果需要的最小总体力值 print(total_cost)

例6:哈夫曼编码问题

问题:给定字符频率,设计最优二进制编码

class HuffmanNode: """哈夫曼树节点类""" def __init__(self, freq, char=None): self.freq = freq # 频率 self.char = char # 字符(叶子节点才有) self.left = None # 左子节点 self.right = None # 右子节点 def __lt__(self, other): """用于堆比较:按频率比较""" return self.freq < other.freq def build_huffman_tree(freq_dict): """ 构建哈夫曼树 贪心策略:每次合并频率最小的两个节点 参数: freq_dict: 字符频率字典 {字符: 频率} 返回: root: 哈夫曼树的根节点 """ if not freq_dict: return None # 创建叶子节点并加入最小堆 heap = [] for char, freq in freq_dict.items(): node = HuffmanNode(freq, char) heapq.heappush(heap, node) # 贪心构建哈夫曼树 while len(heap) > 1: # 取出频率最小的两个节点 left = heapq.heappop(heap) right = heapq.heappop(heap) # 创建新内部节点 merged_freq = left.freq + right.freq merged_node = HuffmanNode(merged_freq) merged_node.left = left merged_node.right = right # 放回堆中 heapq.heappush(heap, merged_node) return heap[0] if heap else None def generate_huffman_codes(root, current_code="", codes={}): """ 生成哈夫曼编码 参数: root: 哈夫曼树根节点 current_code: 当前路径编码 codes: 存储编码的字典 返回: codes: 字符到编码的映射 """ if root is None: return codes # 如果是叶子节点,记录编码 if root.char is not None: codes[root.char] = current_code if current_code else "0" else: # 递归遍历左子树(编码加'0') generate_huffman_codes(root.left, current_code + "0", codes) # 递归遍历右子树(编码加'1') generate_huffman_codes(root.right, current_code + "1", codes) return codes def calculate_compression_rate(original_text, freq_dict, huffman_codes): """ 计算哈夫曼编码的压缩率 参数: original_text: 原始文本 freq_dict: 字符频率 huffman_codes: 哈夫曼编码 返回: compression_rate: 压缩率 """ # 原始比特数(假设8-bit ASCII) original_bits = len(original_text) * 8 if original_text else 0 # 编码后比特数 encoded_bits = 0 for char, freq in freq_dict.items(): code_length = len(huffman_codes.get(char, "")) encoded_bits += freq * code_length if original_bits == 0: return 0 compression_rate = 1 - (encoded_bits / original_bits) return compression_rate # 完整示例 def huffman_coding_example(text): """哈夫曼编码完整示例""" if not text: return None # 1. 统计字符频率 freq_dict = {} for char in text: freq_dict[char] = freq_dict.get(char, 0) + 1 # 2. 构建哈夫曼树 root = build_huffman_tree(freq_dict) # 3. 生成编码 codes = generate_huffman_codes(root) # 4. 计算压缩率 compression_rate = calculate_compression_rate(text, freq_dict, codes) # 5. 编码文本 encoded_text = ''.join(codes[char] for char in text) return { 'frequency': freq_dict, 'huffman_tree': root, 'codes': codes, 'encoded_text': encoded_text, 'compression_rate': compression_rate, 'original_length': len(text) * 8, 'encoded_length': len(encoded_text) } # 使用示例 text = "this is an example for huffman encoding" result = huffman_coding_example(text) print(f"字符频率: {result['frequency']}") print(f"哈夫曼编码: {result['codes']}") print(f"压缩率: {result['compression_rate']:.2%}")

五、贪心算法的适用条件与判断

适用条件:

1、贪心选择性质

1)局部最优选择能导致全局最优解

2)可以通过数学证明验证

2、最优子结构

1)问题的最优解包含子问题的最优解

2)子问题独立且可递归求解

3、无后效性

1)当前决策不影响后续决策的最优性

2)决策序列的代价可加性

贪心算法选择决策树:

开始

问题是否可分解为子问题? → No → 不适合贪心
↓ Yes
子问题是否具有最优子结构? → No → 考虑动态规划
↓ Yes
能否做出局部最优选择? → No → 考虑回溯/分支限界
↓ Yes
局部最优是否保证全局最优? → No → 可能需要其他方法
↓ Yes
✓ 适合使用贪心算法

六、贪心 vs 动态规划对比

对比维度贪心算法动态规划
决策方式局部最优,不可回溯考虑所有可能,可回溯
时间复杂度通常O(n log n)或O(n)通常O(n²)或更高
空间复杂度通常O(1)或O(n)通常O(n²)需要存储状态
解的质量可能不是全局最优保证全局最优
适用问题活动选择、哈夫曼编码、最小生成树0-1背包、最长公共子序列
实现难度相对简单相对复杂

七、贪心算法的实战技巧

技巧1:预处理排序

def greedy_with_sorting(data): """大多数贪心算法需要先排序""" # 关键:确定正确的排序标准 sorted_data = sorted(data, key=custom_key_function) # ... 贪心处理逻辑

技巧2:使用优先队列

def greedy_with_heap(data): """需要频繁获取最小/最大值时使用堆""" import heapq heapq.heapify(data) while condition: current = heapq.heappop(data) # 获取当前最优 # ... 处理逻辑 heapq.heappush(data, new_item) # 更新堆

技巧3:边界条件处理

def robust_greedy(data): """健壮的贪心算法实现""" if not data: # 空输入处理 return default_value # 特殊输入处理 if len(data) == 1: return handle_single_case(data[0]) # 正常贪心逻辑 try: result = greedy_logic(data) except Exception as e: # 异常处理 return fallback_solution(data) return result

八、常见陷阱与避免方法

陷阱1:错误的贪心策略

# 错误示例:0-1背包问题用贪心(单位价值排序) def wrong_knapsack_greedy(weights, values, capacity): items = sorted(zip(weights, values), key=lambda x: x[1]/x[0], reverse=True) total_value = 0 for w, v in items: if capacity >= w: capacity -= w total_value += v return total_value # 可能不是最优解! # 正确:0-1背包应用动态规划

陷阱2:忽略证明

  • ❌ 直接实现看似合理的贪心策略

  • ✅ 先用数学证明或构造反例验证

陷阱3:过度优化

  • ❌ 追求过于复杂的贪心策略

  • ✅ 保持简单,必要时用其他算法补充

九、总结与最佳实践

贪心算法是算法工具箱中的重要工具。正确使用时,它能提供高效的解决方案。总结关键点:

  1. 先证明后实现:确保问题具有贪心选择性质

  2. 从简单开始:先实现基础版本,再优化

  3. 充分测试:用边界用例和随机数据测试

  4. 保持灵活性:当贪心失败时,考虑动态规划等其他方法

总结:

  • 掌握经典的贪心问题模板

  • 理解各种证明方法的适用场景

  • 在实际问题中积累经验,培养对贪心适用性的直觉

(本文例题来自洛谷)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 20:20:25

测试工具创新:驱动软件质量新纪元

创新为何至关重要 在数字化浪潮中&#xff0c;软件已渗透至各行各业&#xff0c;从金融交易到医疗设备&#xff0c;无不依赖高质量代码。然而&#xff0c;传统测试方法如手动测试和脚本化自动化已难以应对日益复杂的系统。测试工具创新通过引入智能化、集成化和用户友好化元素…

作者头像 李华
网站建设 2026/5/26 7:01:43

基于深度学习的石油泄漏检测系统(YOLOv10+YOLO数据集+UI界面+Python项目源码+模型)

一、项目介绍 项目背景: 石油泄漏是环境监测和工业安全中的重要问题&#xff0c;可能对生态系统、人类健康和经济造成严重影响。传统的石油泄漏检测方法通常依赖于人工巡检或传感器监测&#xff0c;效率较低且难以覆盖大面积区域。基于深度学习的目标检测技术能够自动、高效地…

作者头像 李华
网站建设 2026/5/24 5:13:31

研究生必备:6款AI论文生成器实测,提升学术原创性轻松过查重!

如果你是凌晨3点还在凑论文字数的研究生... 是不是每次打开Word都盯着空白页发呆&#xff1f;是不是导师的红笔批注让你一头雾水&#xff08;“逻辑混乱”“缺乏数据支撑”“引用格式错误”&#xff09;&#xff1f;是不是知网查重一次就要花掉半个月的奶茶钱&#xff0c;结果…

作者头像 李华
网站建设 2026/5/22 13:27:28

kanass全面介绍(18) - 如何通过仪表盘,快速直观掌握项目进度及度量

kanass是一款国产开源免费、简洁易用的项目管理工具。不仅具有项目、项目集、迭代、事项等管理功能&#xff0c;还有丰富的图表&#xff0c;用不同的维度展示数据&#xff0c;直观的看出项目等模块进度。1、默认仪表盘1.1 事项统计在系统首页的事项统计区域&#xff0c;放置了事…

作者头像 李华
网站建设 2026/5/25 3:35:17

Samba as Wins Server

自己做的小小實驗 希望能跨網段透過netbios存取同一工作群組下的電腦 Q1 : 同一工作群組在網路芳鄰重新整理會直接出現 還是要連線後才會出現? 用Samba 當作wins server Alpine Linux 安裝samba apk add samba編輯 /etc/samba/smb.conf vi /etc/samba/smb.conf將 wins supp…

作者头像 李华