一、核心原理:什么是贪心?
1.1 定义
贪心:把整体问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直至所有步骤结束。
核心性质:每次采用局部最优,最终结果就全局最优。
1.2 贪心 vs 动态规划
| 对比项 | 贪心 | 动态规划 |
|---|---|---|
| 决策方式 | 当前最优,不回退 | 考虑所有可能,保存最优 |
| 时间复杂度 | 通常 O(n) 或 O(n log n) | 通常 O(n²) 或更高 |
| 适用条件 | 具有贪心选择性质 | 具有最优子结构 |
| 正确性证明 | 需要证明局部最优=全局最优 | 递推公式保证 |
1.3 如何判断能否用贪心?
两个关键性质:
- 最优子结构性质:问题的最优解包含子问题的最优解
- 贪心选择性质:可以通过局部最优的选择得到全局最优
实用技巧:举反例!如果能找到一个反例说明贪心不对,就不能用。
二、经典反例:硬币问题
2.1 能用贪心的情况
硬币:1元、2元、5元,数量不限,支付M元,要求硬币数最少。
贪心策略:每次选最大面值。
支付9元:5+2+2 = 3个硬币 ✓
2.2 不能用贪心的情况
硬币:1元、2元、4元、5元、6元,支付9元。
贪心策略 :6+2+1 = 3个硬币
最优解:4+5 = 2个硬币 ✗
结论:不是所有局部最优都能得到全局最优!
三、贪心经典题型
| 题型 | 核心思想 | 蓝桥杯例题 |
|---|---|---|
| 排序+双指针 | 排序后两头凑 | P532 分箱问题 |
| 堆/优先队列 | 每次取最小/最大 | P545 石子合并 |
| 从左往右扫 | 遇到不同就处理 | P209 翻硬币 |
| 排序后对应 | 一大一小配对 | 数组乘积问题 |
四、例题 1:石子合并(蓝桥杯 P545)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/545/learning/ |
| 类型 | 贪心 + 堆(优先队列) |
| 核心 | 每次选最小的两个合并 |
题目描述
n 个部落,人数分别为 t[i]。每次合并两个部落,花费为两部落人数之和。求合并成一个部落的最小总花费。
贪心证明
关键观察:人数少的部落越早合并,被计算的次数越多(因为它会成为更大部落的一部分,继续参与合并)。
策略:每次选人数最少的两个部落合并,这样小数被多次计算的总代价最小。
证明 :假设当前最小两个为 a <= b,如果先合并其他两个 c <= d,则后续 a 还要再合并,总代价会增加。
完整代码
python
import heapq
n = int(input())
a = list(map(int, input().split()))
# 转换成小根堆
heapq.heapify(a)
ans = 0
while len(a) >= 2:
# 取出两个最小的
x = heapq.heappop(a)
y = heapq.heappop(a)
# 合并
cost = x + y
ans += cost
# 新部落入堆
heapq.heappush(a, cost)
print(ans)
推演过程
n=4, t=[1, 2, 3, 4]
初始堆: [1, 2, 3, 4]
第1轮: 取1,2, 合并=3, ans=3, 堆=[3, 3, 4]
第2轮: 取3,3, 合并=6, ans=9, 堆=[4, 6]
第3轮: 取4,6, 合并=10, ans=19, 堆=[10]
输出: 19
验证其他策略:
如果先合并3,4=7, 堆=[1,2,7]
再合并1,2=3, ans=10, 堆=[3,7]
再合并3,7=10, ans=20, 堆=[10]
总花费20 > 19,贪心更优 ✓
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(n log n) |
堆操作 O(log n),共 n-1 次 |
| 空间 | O(n) |
堆的空间 |
关键细节
| 坑点 | 说明 |
|---|---|
| 必须用堆 | 每次找最小,用堆 O(log n),排序 O(n log n) 但删除麻烦 |
heapq 是小根堆 |
Python 默认小根堆,大根堆存负数 |
| 合并后入堆 | 新部落继续参与后续合并 |
五、例题 2:分箱问题(蓝桥杯 P532)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/532/learning/ |
| 类型 | 贪心 + 排序 + 双指针 |
| 核心 | 大的和小的凑一起,不浪费空间 |
题目描述
纪念品价格上限 w,n 个纪念品,每组最多两件,价格之和 <= w。求最少分组数。
贪心证明
策略:排序后,最便宜的和最贵的凑一组。能凑就凑,不能凑贵的单独一组。
证明 :设最便宜的为 a,最贵的为 b。如果 a + b <= w,则 a 可以和任何人配对(因为其他人都 <= b)。让 a 和 b 配对,不浪费 a 的"配对能力"。如果 a + b > w,则 b 无法和任何人配对(因为 a 是最便宜的),b 必须单独一组。
完整代码
python
w = int(input())
n = int(input())
a = [int(input()) for _ in range(n)]
a.sort()
l, r = 0, n - 1
ans = 0
while True:
if l == r: # 剩一个单独装
ans += 1
break
if l > r: # 装完了
break
if a[l] + a[r] <= w:
ans += 1
l += 1
r -= 1
else:
ans += 1
r -= 1
print(ans)
推演过程
w=10, n=4, a=[1, 2, 8, 9]
排序: [1, 2, 8, 9]
l=0(1), r=3(9): 1+9=10<=10 → 配对, ans=1, l=1,r=2
l=1(2), r=2(8): 2+8=10<=10 → 配对, ans=2, l=2,r=1
l=2>r=1 → 结束
输出: 2
与石子合并的对比
| 对比项 | 石子合并 P545 | 分箱问题 P532 |
|---|---|---|
| 贪心策略 | 每次选最小的两个 | 最小配最大 |
| 数据结构 | 堆(动态维护) | 排序+双指针 |
| 核心思想 | 小数尽早合并,减少重复计算 | 充分利用空间,不浪费配对机会 |
| 时间复杂度 | O(n log n) |
O(n log n) |
六、例题 3:翻硬币(蓝桥杯 P209)
| 项目 | 内容 |
|---|---|
| 链接 | https://www.lanqiao.cn/problems/209/learning/ |
| 类型 | 贪心 + 从左往右扫描 |
| 核心 | 遇到不同就翻,一个位置最多翻一次 |
题目描述
初始状态 s,目标状态 t,每次只能同时翻转相邻两个硬币。求最少操作次数。
贪心证明
关键观察:
- 从左往右第一个不同的位置,必须翻(不翻就永远不同)
- 翻这个位置时,只能连带翻右边相邻的
- 一个位置最多翻一次(翻两次等于没翻)
策略 :从左往右扫描,遇到 s[i] != t[i] 就翻 i 和 i+1。
完整代码
python
s = list(input())
t = list(input())
n = len(s)
ans = 0
for i in range(n - 1): # 最后一个不能作为翻转起点
if s[i] == t[i]:
continue
# 必须翻 i 和 i+1
s[i] = t[i] # 翻后等于目标
# 翻转 i+1
s[i + 1] = '*' if s[i + 1] == 'o' else 'o'
ans += 1
# 检查最后一个是否匹配
if s[-1] == t[-1]:
print(ans)
else:
print(-1) # 无法完成
推演过程
s = "**oo***oooo"
t = "oooo***oooo"
i=0: s[0]='*', t[0]='o', 不同,翻0和1
s[0]='*'→'o', s[1]='*'→'o'
s = "oooo***oooo" = t ✓
ans = 1
输出: 1
关键细节
| 坑点 | 说明 |
|---|---|
| 只能翻相邻两个 | 翻 i 必须连带翻 i+1 |
| 一个位置最多翻一次 | 翻两次等于没翻,所以贪心正确 |
| 最后要检查 | 最后一个位置可能无法匹配 |
| 无法完成输出-1 | 如果最后不匹配,说明无解 |
七、例题 4:数组乘积最小(贪心排序)
| 项目 | 内容 |
|---|---|
| 来源 | 蓝桥云课贪心专题 |
| 类型 | 贪心 + 排序 |
| 核心 | 大配小,小配大 |
题目描述
两个长度为 n 的正整数数组 a 和 b,可以任意排序。求 Σ a[i] * b[i] 的最小值。
贪心证明
策略 :a 从小到大排序,b 从大到小排序,然后对应位置相乘。
证明(以 n=2 为例):
设 a1 < a2, b1 < b2
策略1(大配小):a1*b2 + a2*b1
策略2(大配大):a1*b1 + a2*b2
差值:
(a1*b2 + a2*b1) - (a1*b1 + a2*b2)
= a1*(b2-b1) + a2*(b1-b2)
= (a1-a2)*(b2-b1)
因为 a1 < a2 所以 a1-a2 < 0,b2 > b1 所以 b2-b1 > 0,乘积 < 0。
所以策略1 < 策略2,大配小更优。
完整代码
python
n = int(input())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
a.sort() # a 从小到大
b.sort(reverse=True) # b 从大到小
ans = 0
for i in range(n):
ans += a[i] * b[i]
print(ans)
推演过程
n=3, a=[1, 2, 3], b=[4, 5, 6]
a排序: [1, 2, 3]
b排序: [6, 5, 4]
乘积: 1*6 + 2*5 + 3*4 = 6 + 10 + 12 = 28
验证其他配对:
a=[1,2,3], b=[4,5,6] (同序): 1*4 + 2*5 + 3*6 = 4+10+18 = 32 > 28 ✓
a=[1,2,3], b=[5,4,6]: 1*5 + 2*4 + 3*6 = 5+8+18 = 31 > 28 ✓
八、贪心算法总结
8.1 常见贪心策略
| 策略 | 适用场景 | 例题 |
|---|---|---|
| 排序+双指针 | 配对、分组 | P532 分箱 |
| 堆/优先队列 | 动态取最值 | P545 石子合并 |
| 从左往右扫 | 翻转、覆盖 | P209 翻硬币 |
| 大配小/小配大 | 乘积、差值最小 | 数组乘积 |
| 按结束时间排序 | 区间调度 | 活动安排 |
8.2 贪心正确性验证方法
- 直觉验证:策略看起来合理
- 小规模枚举:n=2,3 时验证所有情况
- 反例寻找:尝试构造让贪心失败的例子
- 交换论证:假设最优解和贪心解不同,通过交换证明贪心不差于最优
九、学习心得
贪心的本质是"短视的智慧"。每一步只看当前最优,但前提是"局部最优=全局最优"。
三句话记住贪心:
- 先排序:80%的贪心题需要先排序
- 举反例:不确定时,构造小数据验证
- 证明难,实用易:竞赛中先写贪心,如果WA再换DP
贪心 vs 动态规划的决策流程:
看到最优问题 → 想贪心 → 举反例验证 → 反例成立?→ 是:用DP;否:用贪心