备战蓝桥杯国赛【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(diffk-1:) ⭐⭐⭐⭐ 差分思想
小蓝的零花钱 前缀和判平衡+贪心选最小 偶+1奇-1→cnt=0可切→代价排序从小到大 cnt更新方向、b>cost不是>=、range(n-1) ⭐⭐⭐⭐ 综合应用
相关推荐
江畔柳前堤2 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore3 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考5 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX5 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面6 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号6 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽6 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI6 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者8 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者8 小时前
【征程6】校准量化中HistogramObserver解析
算法