备战蓝桥杯国赛【Day 17】

📌 写在前面 :今天的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²) 不可接受。

优化思路

  1. 按直径降序排序:直径大的排前面
  2. 逆向遍历 :从直径最大的开始,维护当前见过的最大高度
  3. 判断 :如果当前瓷器的高度 ≥ 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 变大或不变)。

实际上原代码的逻辑是:

  1. 先更新 max_h = max(max_h, dh[i][1])
  2. 然后判断 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个

排序规则:

  1. 封闭图形个数少的排在前面
  2. 如果个数相同,数字本身小的排在前面

给定 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 个位置"切断"
  • 切断的位置:相邻两个玩具的高度差越大,切断后省下的极差越多!

贪心策略

  1. 计算所有相邻高度差diff[i] = h[i] - h[i-1]
  2. 选择最大的 k-1 个差值作为切断点
  3. 最小极差和 = 总极差 - 最大的 k-1 个差值之和

为什么?

  • 整体极差 = h[n-1] - h[0] = diff[1] + diff[2] + ... + diff[n-1]
  • 如果在 diff[i] 处切断,这一段对总极差的贡献就变成0(因为被分成了两段,各自计算极差)
  • 所以要让总极差最小,就要让"省下的极差"最大,即选择最大的 k-1diff

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。diffn-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=Oe=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 的含义 前缀中偶数个数 = 奇数个数,可以切割
切割位置 ii+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) ⭐⭐⭐⭐ 综合应用
相关推荐
kcuwu.7 小时前
决策树与集成学习深度解析:从原理到实践
算法·决策树·集成学习
programhelp_8 小时前
2026 Fall Coinbase Software Engineer OA 真题分享与通关指南
算法
CQU_JIAKE8 小时前
5.19【A】
算法
数智工坊8 小时前
【FDA论文阅读】: 傅里叶域自适应——零训练成本的语义分割无监督域适配方法
论文阅读·人工智能·学习·算法·自动驾驶
Gauss松鼠会8 小时前
【GaussDB】GaussDB 常见问题及解决方案汇总
java·数据库·算法·性能优化·gaussdb·经验总结
炽烈小老头8 小时前
【 每天学习一点算法 2026/05/19】二叉树中的最大路径和
学习·算法
人道领域8 小时前
【LeetCode刷题日记】106.从遍历序列重建二叉树:手撕递归边界,彻底搞懂左闭右闭 vs 左闭右开
java·算法·leetcode
.魚肉8 小时前
Raft 共识算法 · 演示系统(多终端)
算法·go·raft·分布式系统
念恒123068 小时前
Python(while循环)
数据结构·python·算法