【每日一题】贪心

一、核心原理:什么是贪心?

1.1 定义

贪心:把整体问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直至所有步骤结束。

核心性质:每次采用局部最优,最终结果就全局最优。

1.2 贪心 vs 动态规划

对比项 贪心 动态规划
决策方式 当前最优,不回退 考虑所有可能,保存最优
时间复杂度 通常 O(n) 或 O(n log n) 通常 O(n²) 或更高
适用条件 具有贪心选择性质 具有最优子结构
正确性证明 需要证明局部最优=全局最优 递推公式保证

1.3 如何判断能否用贪心?

两个关键性质

  1. 最优子结构性质:问题的最优解包含子问题的最优解
  2. 贪心选择性质:可以通过局部最优的选择得到全局最优

实用技巧:举反例!如果能找到一个反例说明贪心不对,就不能用。


二、经典反例:硬币问题

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/
类型 贪心 + 排序 + 双指针
核心 大的和小的凑一起,不浪费空间

题目描述

纪念品价格上限 wn 个纪念品,每组最多两件,价格之和 <= w。求最少分组数。

贪心证明

策略:排序后,最便宜的和最贵的凑一组。能凑就凑,不能凑贵的单独一组。

证明 :设最便宜的为 a,最贵的为 b。如果 a + b <= w,则 a 可以和任何人配对(因为其他人都 <= b)。让 ab 配对,不浪费 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,每次只能同时翻转相邻两个硬币。求最少操作次数。

贪心证明

关键观察

  1. 从左往右第一个不同的位置,必须翻(不翻就永远不同)
  2. 翻这个位置时,只能连带翻右边相邻的
  3. 一个位置最多翻一次(翻两次等于没翻)

策略 :从左往右扫描,遇到 s[i] != t[i] 就翻 ii+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 的正整数数组 ab,可以任意排序。求 Σ 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 < 0b2 > 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 贪心正确性验证方法

  1. 直觉验证:策略看起来合理
  2. 小规模枚举:n=2,3 时验证所有情况
  3. 反例寻找:尝试构造让贪心失败的例子
  4. 交换论证:假设最优解和贪心解不同,通过交换证明贪心不差于最优

九、学习心得

贪心的本质是"短视的智慧"。每一步只看当前最优,但前提是"局部最优=全局最优"。

三句话记住贪心

  1. 先排序:80%的贪心题需要先排序
  2. 举反例:不确定时,构造小数据验证
  3. 证明难,实用易:竞赛中先写贪心,如果WA再换DP

贪心 vs 动态规划的决策流程

复制代码
看到最优问题 → 想贪心 → 举反例验证 → 反例成立?→ 是:用DP;否:用贪心
相关推荐
IT策士1 小时前
Python 中间件系列:redis 深入浅出
redis·python·中间件
aqiu1111111 小时前
【并查集专题top】
c++·算法
叼烟扛炮2 小时前
C++ 知识点17 友元
开发语言·c++·算法·友员
Dxy12393102162 小时前
Python Pillow库:`img.format`与`img.mode`的区别详解
开发语言·python·pillow
richard_yuu2 小时前
数据结构|二叉树高阶进阶-经典算法
数据结构·c++·算法
不知名的忻2 小时前
Dijkstra算法(朴素版&堆优化版)
java·数据结构·算法··dijkstra算法
༒࿈南林࿈༒2 小时前
刺猬猫小说下载
python·js逆向
星星码️2 小时前
LeetCode刷题简单篇之反转字母
c++·算法·leetcode
.柒宇.2 小时前
AI-Agent入门实战-AI私厨
人工智能·python·langchain·agent·fastapi