📌 写在前面 :今天的4道题全部来自蓝桥杯真题 ,,核心考点包括:贪心策略+排序、自定义比较器、差分思想、前缀和+贪心选择。这些题目看似简单,但暗藏陷阱,是检验"代码实现能力"和"思维细致度"的绝佳素材。
📚 今日刷题清单
| 题号 | 题目 | 来源 | 类型 | 难度 | 核心考点 |
|---|---|---|---|---|---|
| 1 | 阿坤老师的独特瓷器 | 蓝桥杯3994 | 贪心+排序 | ⭐⭐⭐ | 逆向遍历、维护最值、排序比较器 |
| 2 | 封闭图形个数 | 蓝桥杯19733 | 自定义排序 | ⭐⭐⭐ | 多关键字排序、cmp_to_key、数位拆分 |
| 3 | 摆玩具 | 蓝桥杯5888 | 贪心+差分 | ⭐⭐⭐⭐ | 差分思想、贪心选择最大间隙 |
| 4 | 小蓝的零花钱 | 蓝桥杯3236 | 前缀和+贪心 | ⭐⭐⭐⭐ | 前缀和判平衡、贪心选最小代价 |
一、阿坤老师的独特瓷器
1.1 题目描述
阿坤老师有 n 个瓷器,每个瓷器有两个属性:直径 d 和高度 h。
一个瓷器被称为独特瓷器,当且仅当:
- 它的直径
d是所有瓷器中最大的(直径最大的一定是独特瓷器) - 或者 它的高度
h比所有直径比它大的瓷器的高度都要高
换句话说:将瓷器按直径从大到小排序后,如果一个瓷器的高度是当前遍历过的最大值,那它就是独特瓷器。
1.2 关键思路:排序+逆向维护最大值
暴力思路 :对每个瓷器,遍历所有直径比它大的瓷器,检查高度是否最大。时间 O(n²)。
超时原因 :n 可能到 10⁵,O(n²) 不可接受。
优化思路:
- 按直径降序排序:直径大的排前面
- 逆向遍历 :从直径最大的开始,维护当前见过的最大高度
- 判断 :如果当前瓷器的高度
≥ max_h,则它是独特瓷器,更新max_h
为什么逆向遍历?
- 直径最大的瓷器一定是独特瓷器(没有直径比它更大的)
- 从大到小遍历,每个瓷器只需要和"已经遍历过的"(即直径更大的)比较高度
- 用
max_h维护已遍历瓷器的最大高度,O(1) 判断
1.3 推演验证
输入: n=5
瓷器: (d,h) = [(3,5), (1,2), (4,7), (2,6), (5,1)]
按直径降序排序:
(5,1), (4,7), (3,5), (2,6), (1,2)
逆向遍历(从直径最大的开始):
i=0: (5,1), max_h=1, 1>=1 ✓, ans=1
i=1: (4,7), 7>=1 ✓, max_h=7, ans=2
i=2: (3,5), 5<7 ✗
i=3: (2,6), 6<7 ✗
i=4: (1,2), 2<7 ✗
ans = 2
验证:
- (5,1): 直径最大,独特 ✓
- (4,7): 直径比它大的只有(5,1),h=7>1,独特 ✓
- (3,5): 直径比它大的有(5,1),(4,7),max_h=7,5<7,不独特 ✗
- (2,6): 直径比它大的有(5,1),(4,7),(3,5),max_h=7,6<7,不独特 ✗
- (1,2): 直径比它大的有...,max_h=7,2<7,不独特 ✗
1.4 题解
python
n = int(input())
dh = []
for _ in range(n):
d, h = map(int, input().split())
dh.append((d, h))
# 按直径降序排序,直径相同则按高度升序(其实高度无所谓,因为直径相同不会互相比较)
dh = sorted(dh, key=lambda x: (-x[0], x[1]))
ans = 1 # 直径最大的一定是独特瓷器
max_h = dh[0][1] # 当前最大高度
for i in range(1, n):
# 如果当前高度 >= 之前所有(直径更大的)的最大高度
if dh[i][1] >= max_h:
ans += 1
# 更新最大高度
max_h = max(max_h, dh[i][1])
print(ans)
复杂度 :时间 O(n log n)(排序),空间 O(n)
1.5 关键细节
| 坑点 | 说明 |
|---|---|
| 排序方向 | 直径降序 ,所以 key=lambda x: -x[0] |
| 初始值 | ans=1,因为直径最大的瓷器一定是独特瓷器 |
max_h 更新时机 |
先判断再更新,还是先更新再判断?应该是先判断 (用旧max_h),然后更新 |
| 直径相同的情况 | 题目说"直径比它大",所以直径相同的不会互相影响。排序时直径相同的放一起,遍历时会自然处理 |
为什么 dh[i][1] >= max_h 后要 max_h = max(max_h, dh[i][1]) |
其实 dh[i][1] >= max_h 时,max_h = dh[i][1] 就行,但为了代码简洁统一写 max |
1.6 进一步优化思考
原代码有个细节:
python
max_h = max(max_h, dh[i][1])
if dh[i][1] >= max_h:
ans += 1
这里先更新 max_h 再判断,逻辑等价于:
python
if dh[i][1] >= max_h:
ans += 1
max_h = dh[i][1]
else:
max_h = max_h # 不变
因为 max_h 更新后,dh[i][1] >= max_h 仍然成立(max_h 要么不变要么变小... 不对,max_h = max(max_h, dh[i][1]) 会让 max_h 变大或不变)。
实际上原代码的逻辑是:
- 先更新
max_h = max(max_h, dh[i][1]) - 然后判断
dh[i][1] >= max_h
如果 dh[i][1] > max_h(旧),则 max_h 更新为 dh[i][1],然后 dh[i][1] >= max_h 成立(等于)。
如果 dh[i][1] == max_h(旧),则 max_h 不变,dh[i][1] >= max_h 成立(等于)。
如果 dh[i][1] < max_h(旧),则 max_h 不变,dh[i][1] >= max_h 不成立。
所以原代码等价于 if dh[i][1] >= max_h_old: ans += 1; max_h = max(max_h_old, dh[i][1]),逻辑正确。
但建议写成更清晰的形式:
python
for i in range(1, n):
if dh[i][1] >= max_h:
ans += 1
max_h = dh[i][1]
二、封闭图形个数
2.1 题目描述
蓝桥王国的数字大小规则很特别:一个数字的"大小"由它的封闭图形个数决定。
每个数字的封闭图形个数:
0, 4, 6, 9:1个8:2个1, 2, 3, 5, 7:0个
排序规则:
- 封闭图形个数少的排在前面
- 如果个数相同,数字本身小的排在前面
给定 n 个数字,按此规则排序输出。
2.2 关键思路:多关键字排序
核心:自定义比较函数,先比封闭图形个数,个数相同再比数值大小。
Python实现 :使用 functools.cmp_to_key 将比较函数转为 key 函数。
2.3 推演验证
输入: n=3, a=[18, 29, 6]
计算封闭图形个数:
18: 1(1个) + 0(8有2个?) 等等,重新算
1: 0个, 8: 2个 → 18有2个
2: 0个, 9: 1个 → 29有1个
6: 1个 → 6有1个
排序:
29: 1个
6: 1个(个数相同,6<29,所以6在前)
18: 2个
输出: 6 29 18 ✓
2.4 题解
python
from functools import cmp_to_key
# 每个数字的封闭图形个数
ls = [1, 0, 0, 0, 1, 0, 1, 0, 2, 1]
# 索引: 0 1 2 3 4 5 6 7 8 9
n = int(input())
a = list(map(int, input().split()))
def compare(a, b):
"""
比较函数:返回负数表示a<b(a排前面),正数表示a>b,0表示相等
"""
# 计算a的封闭图形个数
numa = sum([ls[int(i)] for i in str(a)])
# 计算b的封闭图形个数
numb = sum([ls[int(i)] for i in str(b)])
# 先比封闭图形个数
if numa < numb:
return -1
elif numa > numb:
return 1
# 个数相同,比数字本身大小
if a < b:
return -1
elif a > b:
return 1
else:
return 0
# 使用cmp_to_key进行自定义排序
a.sort(key=cmp_to_key(compare))
print(*a)
复杂度 :时间 O(n log n × digit),digit 是数字位数(通常很小),空间 O(n)
2.5 关键细节
| 坑点 | 说明 |
|---|---|
cmp_to_key |
Python3 取消了 sort(cmp=...),必须用 functools.cmp_to_key |
| 比较函数返回值 | 返回负数表示 a<b(a排前面),不是返回 True/False |
| 数字转字符串 | str(a) 拆分成每一位,再转 int 查表 |
ls 数组索引 |
ls[0]=1(0有1个封闭图形),ls[8]=2(8有2个) |
| 负数处理 | 如果输入有负数,str(-5) 会得到 '-5',int('-') 会报错。但题目说正整数,不用考虑 |
2.6 优化版本:预计算
如果 n 很大,每次比较都计算封闭图形个数会重复。可以预计算:
python
from functools import cmp_to_key
ls = [1, 0, 0, 0, 1, 0, 1, 0, 2, 1]
n = int(input())
a = list(map(int, input().split()))
# 预计算每个数字的封闭图形个数
circle_count = {}
for num in a:
if num not in circle_count:
circle_count[num] = sum(ls[int(i)] for i in str(num))
def compare(a, b):
ca, cb = circle_count[a], circle_count[b]
if ca != cb:
return -1 if ca < cb else 1
return -1 if a < b else (1 if a > b else 0)
a.sort(key=cmp_to_key(compare))
print(*a)
三、摆玩具
2.1 题目描述
小蓝有 n 个玩具,按高度从矮到高 摆放在窗台上。他想分成 k 段,使得所有分段的极差之和尽可能小。
极差:一段中最高和最矮的高度之差。
输入 :n, k 和高度数组 h(已排序)
输出:最小极差和
3.2 关键思路:差分+贪心选最大间隙
核心观察:
- 数组已经升序排列
- 如果不分段(
k=1),极差和 =h[n-1] - h[0](整体极差) - 如果分成
k段,需要在k-1个位置"切断" - 切断的位置:相邻两个玩具的高度差越大,切断后省下的极差越多!
贪心策略:
- 计算所有相邻高度差 :
diff[i] = h[i] - h[i-1] - 选择最大的
k-1个差值作为切断点 - 最小极差和 = 总极差 - 最大的
k-1个差值之和
为什么?
- 整体极差 =
h[n-1] - h[0] = diff[1] + diff[2] + ... + diff[n-1] - 如果在
diff[i]处切断,这一段对总极差的贡献就变成0(因为被分成了两段,各自计算极差) - 所以要让总极差最小,就要让"省下的极差"最大,即选择最大的
k-1个diff
3.3 推演验证
输入: n=5, k=2, h=[2, 5, 7, 10, 13]
相邻差值:
diff = [3, 2, 3, 3] (5-2=3, 7-5=2, 10-7=3, 13-10=3)
k=2,需要选1个最大差值切断
最大差值 = 3(有3个都是3,任选一个)
如果不切断,极差 = 13-2 = 11
切断后(比如在第一个3处切断):
段1: [2,5], 极差=3
段2: [7,10,13], 极差=6
总和 = 3+6 = 9
用公式:总极差 - 最大差值 = 11 - 3 = 8? 不对...
等等,重新理解:
整体极差 = 13-2 = 11
diff总和 = 3+2+3+3 = 11 ✓
如果在diff[0]=3处切断(即在2和5之间切断):
段1: [2], 极差=0
段2: [5,7,10,13], 极差=8
总和 = 0+8 = 8
用公式:总极差 - 切断的差值 = 11 - 3 = 8 ✓
样例输出是8 ✓
3.4 题解
python
n, k = map(int, input().split())
h = list(map(int, input().split()))
# 已经按高度排序(题目说从矮到高摆放)
h.sort()
# 计算相邻差值
diff = []
for i in range(1, n):
diff.append(h[i] - h[i-1])
# 贪心:选最大的k-1个差值作为切断点
diff.sort(reverse=True)
# 总极差 = 最后一个 - 第一个 = h[n-1] - h[0]
# 也可以 = sum(diff)
total = h[-1] - h[0]
# 减去最大的k-1个差值
for i in range(k - 1):
total -= diff[i]
print(total)
更简洁的版本:
python
n, k = map(int, input().split())
h = list(map(int, input().split()))
h.sort()
diff = [h[i] - h[i-1] for i in range(1, n)]
diff.sort(reverse=True)
# 或者直接 sum(diff[k-1:])
print(sum(diff[k-1:]))
复杂度 :时间 O(n log n)(排序),空间 O(n)
3.5 关键细节
| 坑点 | 说明 |
|---|---|
h.sort() |
题目说"从矮到高",但输入不一定有序,必须排序 |
diff 长度 |
n-1 个差值,k 段需要 k-1 个切断点 |
k=1 的情况 |
range(0) 不执行循环,total 不变,正确 |
k=n 的情况 |
每个玩具一段,极差和为0。diff 选 n-1 个,剩下0个,sum([])=0 ✓ |
| 排序方向 | diff.sort(reverse=True) 降序,选前 k-1 个最大的 |
3.6 本质理解
整体极差 = diff[0] + diff[1] + ... + diff[n-2]
分成k段 = 选k-1个位置切断
切断后,这k-1个diff不再计入总极差
所以:最小极差和 = sum(diff) - max(k-1个diff)
= 剩余(n-k)个diff的和
这就是代码 sum(diff[k-1:]) 的含义:去掉最大的 k-1 个,剩下的求和。
四、小蓝的零花钱
4.1 题目描述
小蓝有一个长度为 n 的序列,奇数和偶数的数量相等。
他可以在某个位置切割 ,将序列分成两段,要求每段中奇数和偶数的数量都相等 。切割的代价 = 切割位置两端元素的差的绝对值 |a[i] - a[i+1]|。
小蓝有 B 元零花钱,求最多能切割几次。
输入 :n, B 和序列 a
输出:最多切割次数
4.2 关键思路:前缀和判平衡+贪心选最小代价
第一步:判断可切割位置
什么位置可以切割?
- 切割后,左边一段奇偶数量相等,右边一段奇偶数量相等
- 因为整体奇偶数量相等 ,所以如果左边相等,右边一定也相等!
证明:
- 设整体有
E个偶数,O个奇数,E = O - 左边有
e个偶数,o个奇数,若e = o - 右边有
E-e个偶数,O-o个奇数 - 因为
E=O且e=o,所以E-e = O-o,右边也相等 ✓
所以只需要判断:前缀中奇数和偶数是否相等!
第二步:用前缀和找切割点
- 遍历序列,维护
cnt:遇到偶数+1,遇到奇数-1 - 当
cnt == 0时,说明当前位置前缀中奇偶数量相等,可以切割! - 切割代价 =
|a[i] - a[i+1]|
第三步:贪心选择
- 可能有多个可切割位置,每个位置有不同的代价
- 要最大化切割次数 ,就要优先选择代价小的位置
- 将所有可切割位置的代价排序,从小到大选,直到预算用完
4.3 推演验证
输入: n=6, B=3, a=[1, 2, 3, 4, 5, 6]
遍历找切割点:
i=0: a[0]=1(奇), cnt=-1
i=1: a[1]=2(偶), cnt=-1+1=0 → 可以切割!代价=|2-3|=1
i=2: a[2]=3(奇), cnt=0-1=-1
i=3: a[3]=4(偶), cnt=-1+1=0 → 可以切割!代价=|4-5|=1
i=4: a[4]=5(奇), cnt=0-1=-1
可切割位置: [1, 1](代价都是1)
排序: [1, 1]
B=3:
选第一个: B=3-1=2, ans=1
选第二个: B=2-1=1, ans=2
输出: 2 ✓
4.4 题解
python
n, b = map(int, input().split())
a = list(map(int, input().split()))
cnt = 0 # 前缀中(偶数个数 - 奇数个数),为0表示奇偶数量相等
res = [] # 存储所有可切割位置的代价
# 遍历到n-2,保证i+1不越界
for i in range(n - 1):
if a[i] % 2 == 0:
cnt += 1
else:
cnt -= 1
# 前缀奇偶数量相等,可以在这里切割
if cnt == 0:
res.append(abs(a[i] - a[i + 1]))
# 贪心:优先选择代价小的
res.sort()
ans = 0
for cost in res:
if b > cost: # 注意:原代码是 > 不是 >=,严格大于才切割
b -= cost
ans += 1
else:
break
print(ans)
复杂度 :时间 O(n log n)(排序 res),空间 O(n)
4.5 关键细节
| 坑点 | 说明 |
|---|---|
cnt 的更新 |
偶数 +1,奇数 -1,不是反过来! |
cnt == 0 的含义 |
前缀中偶数个数 = 奇数个数,可以切割 |
| 切割位置 | 在 i 和 i+1 之间切割,代价是 ` |
b > cost 还是 b >= cost |
原代码是 >,严格大于。如果 b == cost,不切割(切割后b=0,但代码逻辑是不切) |
range(n-1) |
遍历到倒数第二个元素,因为切割需要 i+1 |
| 为什么整体奇偶相等就能保证 | 前缀和为0时,左边奇偶相等;整体奇偶相等,所以右边也相等 |
4.6 进一步优化
如果 n 很大但 B 很小,可以用**优先队列(最小堆)**动态维护,但本题 n ≤ 100,直接排序即可。
边界情况:
n=2:只有一个切割位置(如果奇偶各一个)- 没有可切割位置:
res为空,输出0
🎯 今日复盘总结
| 题目 | 核心技巧 | 思维路径 | 易错点 | 国赛价值 |
|---|---|---|---|---|
| 独特瓷器 | 排序+逆向维护最值 | 直径降序→从大到小遍历→维护最大高度 | 排序方向、初始ans=1、max_h更新时机 | ⭐⭐⭐ 经典贪心 |
| 封闭图形个数 | 自定义排序 | 数位拆分→查表→多关键字排序 | cmp_to_key用法、比较函数返回值 | ⭐⭐⭐ 基础技巧 |
| 摆玩具 | 差分+贪心选最大间隙 | 升序排列→算相邻差→选最大k-1个切断 | diff长度n-1、排序方向、sum(diff[k-1:]) | ⭐⭐⭐⭐ 差分思想 |
| 小蓝的零花钱 | 前缀和判平衡+贪心选最小 | 偶+1奇-1→cnt=0可切→代价排序从小到大 | cnt更新方向、b>cost不是>=、range(n-1) | ⭐⭐⭐⭐ 综合应用 |