写在前面
今天完成了蓝桥杯的五道真题,从螺旋矩阵的边界控制,到图像模糊的卷积模拟,再到日期回文的数学规律,冰雹数的记忆化剪枝,最后是多源BFS的扩散模型。
这五道题恰好构成了一条从暴力到优化的完整学习路径。记录如下,既是复盘,也希望对同样备战蓝桥杯的你有所启发。
第一题:螺旋矩阵
题目大意
给定 n × m n \times m n×m 的矩阵,按顺时针螺旋方式填入 1 1 1 到 n × m n \times m n×m,求第 r r r 行第 c c c 列的数字。
核心思路:边界碰撞法
螺旋填充的本质是方向优先级 :右 → 下 → 左 → 上,循环往复。关键在于何时转向 ------当前进方向的下一个格子越界或已填充时,立即转向。
代码实现
python
n, m = map(int, input().split())
r, c = map(int, input().split())
ans = [[0] * m for _ in range(n)]
x, y = 0, 0
value = 1
ans[x][y] = value
while value < n * m:
# 右 → 下 → 左 → 上,顺时针螺旋
while y + 1 < m and ans[x][y + 1] == 0:
y += 1
value += 1
ans[x][y] = value
while x + 1 < n and ans[x + 1][y] == 0:
x += 1
value += 1
ans[x][y] = value
while y - 1 >= 0 and ans[x][y - 1] == 0:
y -= 1
value += 1
ans[x][y] = value
while x - 1 >= 0 and ans[x - 1][y] == 0:
x -= 1
value += 1
ans[x][y] = value
print(ans[r - 1][c - 1])
深度思考:为什么这样写最清晰?
很多初学者喜欢用 dx, dy 方向数组配合 dir 变量转向,但这道题四个方向的判断条件不同 (右看列边界,下看行边界),用四个独立的 while 反而语义最清晰。
关键细节 :ans[x][y+1] == 0 这个条件同时承担了边界检查 和访问检查两个职责,是螺旋算法的精髓。
时间复杂度
O ( n m ) O(nm) O(nm),每个格子恰好访问一次。
第二题:图像模糊
题目大意
对 n × m n \times m n×m 的灰度图像做均值滤波:每个像素替换为其 3 × 3 3 \times 3 3×3 邻域(含边界不足9个的情况)的平均值(下取整)。
核心思路:卷积核模拟
这是计算机视觉中最基础的均值模糊 (Box Blur)。对于每个像素 ( i , j ) (i,j) (i,j),需要统计其 3 × 3 3 \times 3 3×3 邻域内的有效像素和个数。
代码实现
python
n, m = map(int, input().split())
img = [list(map(int, input().split())) for _ in range(n)]
# 9个方向的偏移量(包含自身)
dirs = [(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 0), (0, 1),
(1, -1), (1, 0), (1, 1)]
ans = [[0] * m for _ in range(n)]
for i in range(n):
for j in range(m):
total, cnt = 0, 0
for dx, dy in dirs:
x, y = i + dx, j + dy
if 0 <= x < n and 0 <= y < m: # 边界检查
total += img[x][y]
cnt += 1
ans[i][j] = total // cnt # 下取整
for row in ans:
print(*row)
深度思考:边界处理的优雅写法
注意 0 <= x < n 这种链式比较 是 Python 的语法,比 x >= 0 and x < n 更简洁。
为什么用 // 而不是 int()?
// 是向下取整除法,对于正数等同于 int(a/b),但速度更快。
扩展:高斯模糊怎么做?
如果 weights 不是均匀的,而是呈高斯分布(中心权重高,边缘权重低),则:
python
weights = [[1, 2, 1],
[2, 4, 2],
[1, 2, 1]] # 高斯核
total = sum(weights[x][y] * img[i+dx][j+dy] for ...)
ans[i][j] = total // sum(sum(row) for row in weights)
第三题:回文日期
题目大意
给定日期 N N N(8位整数,如 20200202),求:
- 下一个普通回文日期(如
20211202) - 下一个
ABABBABA型回文日期(如21211212)
核心思路:日期迭代 + 字符串回文判断
代码实现
python
from datetime import datetime, timedelta
n = int(input())
start = datetime.strptime(str(n), '%Y%m%d').date()
delta = timedelta(days=1)
found_plain = False # 是否已找到普通回文
while True:
start += delta
date_str = start.strftime('%Y%m%d')
# 普通回文:正读反读相同
if not found_plain and date_str == date_str[::-1]:
print(date_str)
found_plain = True
# ABABBABA 型:位置 0,2,4,6 相同,位置 1,3,5,7 相同
# 即 date_str[0]==date_str[2]==date_str[4]==date_str[6]
# 且 date_str[1]==date_str[3]==date_str[5]==date_str[7]
if (date_str[0] == date_str[2] == date_str[4] == date_str[6] and
date_str[1] == date_str[3] == date_str[5] == date_str[7]):
print(date_str)
break
深度思考:为什么不用数学法?
理论上可以直接构造回文日期(年决定日,月需合法),但日期合法性判断极其繁琐 (闰年、大小月、二月天数)。用 datetime 迭代虽然看似暴力,但:
- 代码极短,不易出错
- 日期跨度有限(下一个回文日期不会太远)
- Python 的日期库足够快
ABABBABA 的本质 :YYYYMMDD 中,Y[0]==Y[2]==M[0]==D[0] 且 Y[1]==Y[3]==M[1]==D[1],即年份前两位决定一切。
优化方向:数学构造
若数据范围极大(如求第 10 6 10^6 106 个回文日期),则需数学法:
- 枚举年份 Y Y Y(4位)
- 构造回文:
Y * 10000 + reverse(Y) - 检查月日合法性
第四题:冰雹数(重点!)
题目大意
对任意正整数 N N N,如果是偶数则 N / 2 N/2 N/2,如果是奇数则 3 N + 1 3N+1 3N+1,最终必到 1 1 1。求 2 2 2 到 N N N 中,变换过程中出现的最大值。
初版:暴力模拟(TLE风险)
python
n = int(input())
max_num = n
for start in range(2, n + 1):
x = start
while x != 1:
if x % 2 == 0:
x //= 2
else:
x = x * 3 + 1
max_num = max(max_num, x)
print(max_num)
问题 : N < 10 6 N < 10^6 N<106,但冰雹序列可能极长(如 N = 77031 N=77031 N=77031 时序列长度达 350 350 350),总复杂度接近 O ( N × L ) O(N \times L) O(N×L),可能超时。
优化版:记忆化剪枝
关键洞察 :若计算 N = 10 N=10 N=10 时,序列经过 5 5 5,而 N = 5 N=5 N=5 的结果已计算过,则可直接复用。
但本题求的是过程中的最大值而非序列长度,剪枝策略需调整:
python
n = int(input())
max_num = n
for start in range(2, n + 1):
x = start
while x != 1:
if x % 2 == 0:
x //= 2
else:
x = x * 3 + 1
max_num = max(max_num, x) # 奇数变换后可能更大
# 剪枝:若 x 已小于 start,且 start 之前已遍历过
# 则 x 的最大值不可能超过当前 max_num
if x < start:
break
print(max_num)
深度思考:剪枝的正确性证明
核心观察 :对于 x < s t a r t x < start x<start,我们在遍历 s t a r t start start 时, x x x 作为更小的起始值已经被处理过 。其产生的最大值已经被纳入 max_num 的考虑范围。
因此,一旦当前序列降落到 x < s t a r t x < start x<start,就可以安全终止,因为:
- x x x 的后续序列是确定的
- x x x 序列中的最大值已在之前计算时考虑
- 无需重复计算
这本质上是一种自底向上的动态规划思想。
更优解:全局记忆化
python
from functools import lru_cache
@lru_cache(maxsize=None)
def hailstone_max(x):
if x == 1:
return 1
if x % 2 == 0:
nxt = x // 2
else:
nxt = x * 3 + 1
return max(x, hailstone_max(nxt))
n = int(input())
print(max(hailstone_max(i) for i in range(1, n + 1)))
第五题:长草(重点!多源BFS)
题目大意
n × m n \times m n×m 的草地,初始有些格子有草。每月草向上下左右扩散一格,求 k k k 月后的状态。
初版:暴力模拟(过掉80%)
python
n, m = map(int, input().split())
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
grid = [list(input()) for _ in range(n)]
k = int(input())
for _ in range(k):
new_grid = [row.copy() for row in grid]
for i in range(n):
for j in range(m):
if grid[i][j] == 'g':
for dx, dy in dirs:
x, y = i + dx, j + dy
if 0 <= x < n and 0 <= y < m:
new_grid[x][y] = 'g'
grid = new_grid
for row in grid:
print(''.join(row))
问题 : k ≤ 1000 k \leq 1000 k≤1000,每月全图扫描 O ( n m ) O(nm) O(nm),总复杂度 O ( k n m ) O(knm) O(knm),对于 1000 × 1000 1000 \times 1000 1000×1000 的极限数据会超时。
优化版:多源BFS
核心洞察 :草的扩散是同时的、均匀的,这完全符合BFS的层级遍历特性。
- 初始所有草的位置作为多源起点
- 每层BFS恰好对应一个月的扩散
- 每个空地只被访问一次,总复杂度 O ( n m ) O(nm) O(nm)
python
from collections import deque
n, m = map(int, input().split())
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
grid = [list(input()) for _ in range(n)]
k = int(input())
# 多源BFS初始化:所有草同时入队
q = deque()
for i in range(n):
for j in range(m):
if grid[i][j] == 'g':
q.append((i, j))
def bfs_layer():
"""扩散一层(一个月)"""
size = len(q)
for _ in range(size):
x, y = q.popleft()
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < m and grid[nx][ny] == '.':
grid[nx][ny] = 'g'
q.append((nx, ny))
# 执行 k 个月
for _ in range(k):
bfs_layer()
for row in grid:
print(''.join(row))
深度思考:为什么BFS更优?
| 维度 | 暴力模拟 | 多源BFS |
|---|---|---|
| 每月操作 | 全图扫描 O ( n m ) O(nm) O(nm) | 仅处理边界草 O ( perimeter ) O(\text{perimeter}) O(perimeter) |
| 重复访问 | 已长草的格子反复判断 | 每个格子仅入队一次 |
| 总复杂度 | O ( k n m ) O(knm) O(knm) | O ( n m ) O(nm) O(nm) |
| 空间复杂度 | O ( n m ) O(nm) O(nm) 复制数组 | O ( n m ) O(nm) O(nm) 队列 |
BFS的本质 :将"时间"维度转化为"图上的距离"。每个空地被草覆盖的月份 = 它到最近初始草地的曼哈顿距离。
终极优化:直接计算距离
若只问最终状态,甚至可以不用模拟:
python
from collections import deque
n, m = map(int, input().split())
grid = [list(input()) for _ in range(n)]
k = int(input())
# 计算每个空地到最近草地的距离
dist = [[-1] * m for _ in range(n)]
q = deque()
for i in range(n):
for j in range(m):
if grid[i][j] == 'g':
q.append((i, j))
dist[i][j] = 0
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while q:
x, y = q.popleft()
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < m and dist[nx][ny] == -1:
dist[nx][ny] = dist[x][y] + 1
q.append((nx, ny))
# 距离 <= k 的格子都长草
for i in range(n):
for j in range(m):
if grid[i][j] == 'g' or dist[i][j] <= k:
print('g', end='')
else:
print('.', end='')
print()
这揭示了问题的本质 :多源BFS一次求出所有最短距离,之后 O ( 1 ) O(1) O(1) 判断每个格子。
五题复盘:算法思想的递进
| 题号 | 题目 | 核心算法 | 优化关键词 |
|---|---|---|---|
| 1 | 螺旋矩阵 | 模拟 | 边界碰撞、访问标记 |
| 2 | 图像模糊 | 卷积模拟 | 方向数组、链式比较 |
| 3 | 回文日期 | 枚举 | 日期库、字符串回文 |
| 4 | 冰雹数 | 模拟 + 剪枝 | 记忆化、自底向上 |
| 5 | 长草 | 多源BFS | 层级遍历、距离转化 |
给蓝桥杯选手的建议
1. 先暴力,再优化
竞赛中先写出暴力解法拿部分分,再思考优化。第5题的80分暴力在考场上完全可先提交。
2. 识别问题结构
- 看到"同时扩散" → 想BFS
- 看到"重复子问题" → 想DP/记忆化
- 看到"网格遍历" → 想方向数组 + DFS/BFS
3. Python的竞争力
Python在蓝桥杯完全够用,关键是:
- 熟练使用
deque做BFS - 熟练使用
datetime处理日期 - 熟练使用列表推导式和切片
4. 边界意识
蓝桥杯很多分丢在边界:
- 螺旋矩阵的
0起始 vs1起始 - 图像模糊的边界不足9个像素
- 日期迭代的
while True终止条件
结语
五道题,从螺旋到回文,从冰雹到长草,看似各异,实则相通:识别结构,选择工具,优雅实现。
算法竞赛的魅力不在于写出最复杂的代码,而在于用最简洁的代码,表达最清晰的思路。
"代码是写给人看的,顺便给机器执行。"
------ Harold Abelson
愿你在蓝桥杯的赛场上,也能写出这样优雅的代码。
如需调整某个题目的分析深度,或补充更多图示说明,请告诉我!