
1. 赛前先想清楚为什么 Python 选手更需要模板打蓝桥杯的人里用 Python 的那批心态其实挺微妙的。C 选手觉得自己慢工出细活Java 选手觉得 API 齐全Python 选手最大的底气就是写得快——但最大的焦虑也是快得没边快到最后忘了边界、忘了格式、忘了大数据量会不会 TLE。我自己从第一次参赛到现在最大的感受就是模板不是让你偷懒是让你把注意力从怎么写挪到怎么想。蓝桥杯官方给的 Python 环境和本地其实是有差异的历年真题里像蚂蚁感冒这类模拟题、2022 国 B 的出差这类最短路题考的都是能不能在有限时间里把标准解法敲对。而你一旦在赛场上从零手写并查集、从零搭 BFS 骨架那几分钟的犹豫加上一个 lower_bound 的边界没抠准基本就告别省一了。我给自己定过一条纪律凡是两周内写过两遍以上的代码结构一律沉淀成模板。下面这套东西就是按这个标准攒出来的覆盖输入输出、基础工具、数据结构、搜索图论、动态规划、数论六个方向你可以直接拿去用也可以按自己的书写习惯改。1.1 蓝桥杯赛制对 Python 的友好与不友好蓝桥杯软件类的题目分填空题和编程题填空题的答案往往是一个数字或一个字符串你就算用 Python 暴力枚举也能出结果这类题其实是 Python 的主场——一行itertools.permutations就能省掉 C 选手半小时的递归。不友好的地方在于运行时限和递归深度。Python 的常数因子大概是 C 的 30 到 50 倍一个 1e8 级别的循环C 一秒过Python 得七八秒。所以模板里那些能少一个循环就少一个循环的写法不是炫技是被逼出来的。另外 Python 默认递归深度上限是 1000DFS 深搜稍微深一点就RecursionError所以我在每个模板文件的头部都会加一句sys.setrecursionlimit(10**6)这一句能救回至少一道题。还有一个很多人忽视的点蓝桥杯是按测试点给分的不是全对才有分。也就是说哪怕你的正解只过了前 60% 的数据你也是有分的。所以模板设计的第一原则不是最优而是**能过就先用时间够再优化**。1.2 模板要分成三层别一股脑塞进一个文件新手常见做法是把所有模板抄在一个template.py里几百行赛场上翻都翻不到。我的做法是分三层一层是 IO 层读输入的几种写法、快速输出这几行必须闭着眼睛能敲。二层是工具层排序、二分、前缀和、并查集、堆、Counter这些是随时可能用到的通用件。三层是算法层BFS/DFS、Dijkstra、背包、筛法这类是题目点出来了才用的。三层分开写在三个文件里赛前各扫一遍比赛时按需粘。这样做的好处是你面对一道题时脑子里第一个反应是这题属于哪一层而不是翻代码。2. 输入输出模板决定你能不能拿到第一分我见过太多人算法想对了最后栽在输入输出上。蓝桥杯的输入格式描述有时候写得比较含糊第一行输入两个整数 n 和 m这种还好怕的是接下来若干行每行若干个整数——若干两个字就是坑你得自己判断是读到文件结束还是读到 n 行为止。我的经验是先把输入完整读进来再在内存里切分。这样你只需要维护一套解析逻辑不用管是单行多行。核心就是sys.stdin.read().split()这一句它把整个输入按空白字符切开变成一个字符串列表速度还比input()循环快好几倍。2.1 三种输入形态的读法第一种是固定行数、每行固定个数。这种最省事用map(int, input().split())一行搞定import sys n, m map(int, sys.stdin.readline().split()) a list(map(int, sys.stdin.readline().split()))第二种是第一行给行数后面每行不定长。这时候别逐行input()直接在读全量数据后按下标游标推进import sys def main(): data sys.stdin.read().split() idx 0 n int(data[idx]); idx 1 m int(data[idx]); idx 1 # 例如后面是一张 n 行 m 列的网格 grid [] for i in range(n): row data[idx: idx m] idx m grid.append(row) # ... 后续处理 main()这种游标推进的写法我强烈推荐。原因很实在当题目给的输入一部分是整数、一部分是字符比如网格里混着.和#你没法统一转 int游标法是唯一能同时应付两种类型的方式。第三种是读到 EOF 为止。蓝桥杯有些题不给数量直接给一串数据。这种用for line in sys.stdin:最稳注意每行末尾的换行符要strip()掉。注意sys.stdin.read()之后 stdin 就被读空了所以整个程序里只能调用一次。如果你的逻辑要分函数处理记得把 data 作为参数传下去不要在函数里再 read 一遍。2.2 输出格式的隐形陷阱输出这块有三个高频扣分点我按踩坑频率排个序陷阱具体表现规避写法多余的换行循环里逐个print最后多一个空行用 .join(map(str, ans))一次输出该换行没换行多组答案拼在一行每组答案单独sys.stdout.write(...\n)浮点精度print(3.0)输出3.0题目要的是3按题意决定是否int()或格式化关于浮点多说一句。蓝桥杯的填空题很多答案是保留两位小数这时候用print(f{x:.2f})但如果是编程题通常是输出一个整数那你就得想清楚中间过程用不用浮点。我个人建议能用整数运算就别碰浮点比如比较a/b c/d写成a*d c*b避开了精度问题还更快。2.3 大数据量下的读写加速这一条是 Python 选手的命根子。当数据量到 1e5、1e6 级别的时候input()和print()的开销会变成主要耗时。标准做法是把input换成sys.stdin.readline把print换成sys.stdout.write然后在 main 里统一缓冲import sys input sys.stdin.readline out [] def main(): n int(input()) for _ in range(n): v int(input()) out.append(str(v * 2)) sys.stdout.write(\n.join(out)) main()为什么把结果先攒到 list 里再一次性写出而不是边算边 write因为单次write也有系统调用开销攒起来一次写出能把输出耗时压到原来的几分之一。这个技巧在我做的几道 1e5 规模的排序题上实测差别很明显具体数字跟环境有关但方向一定是对的。3. 基础工具模板排序、二分、前缀和与双指针这一层是用得最多、最不容易写错、但一旦错了最不容易发现的部分。为什么这么说因为它们的错误往往是边界差一位而边界差一位在样例上跑得完全正常只有大数据才会挂。所以这一层的模板重点不是写法本身而是边界的固定策略。3.1 排序与自定义比较Python 的sort是稳定的 Timsort平均和最坏都是 O(n log n)直接放心用。真正常用的是自定义排序元组排序天然按元素顺序比较所以多关键字排序直接用元组就行不需要写比较函数。# 先按分数降序分数相同按名字升序 people [(bob, 90), (alice, 95), (carol, 90)] people.sort(keylambda x: (-x[1], x[0]))这里的关键技巧是**降序用负号**。很多人写reverseTrue配上多关键字就懵了因为reverseTrue会把所有关键字一起翻转。用负号只翻转你想翻转的那一维思路清晰还不容易错。注意这个技巧只对数值类型有效字符串没法取负字符串降序就只能老老实实排两遍或者用functools.cmp_to_key。心得cmp_to_key我一般只在真的没法转成元组比较时才用因为它每次比较都要回调 Python 函数速度比 key 排序慢不少。能转元组就转元组。3.2 二分的三个模板与边界处理二分的坑我总结成一句话你写的是找第一个满足条件的位置还是找最后一个满足条件的位置这两件事必须一开始就想清楚。想清楚之后后半段就是机械执行。下面是我固定的两个写法# 写法一找第一个 x 的位置左边界 def lower_bound(a, x): lo, hi 0, len(a) # 答案在 [lo, hi] 闭区间 while lo hi: mid (lo hi) // 2 if a[mid] x: hi mid else: lo mid 1 return lo # 返回 len(a) 表示没找到 # 写法二找最后一个 x 的位置右边界 def upper_bound(a, x): lo, hi -1, len(a) - 1 while lo hi: mid (lo hi 1) // 2 # 注意这里 1 if a[mid] x: lo mid else: hi mid - 1 return lo # 返回 -1 表示没找到第二个写法里的1是最容易被漏的地方。为什么必须加因为当区间只剩两个元素时如果不加 1mid会一直等于lo而如果这时走的是lo mid分支区间就不会收缩直接死循环。加 1 让mid向上取整保证区间一定会缩小。这个道理我在纸上画了三次才彻底记住。二分还有一个隐藏用法答案二分。题目问最小的时间最大的容量这类往往是让你二分答案再校验可行性。这时校验函数写对是重点二分框架就直接套上面的写法一。3.3 前缀和与差分前缀和的用途是把区间求和从 O(n) 降到 O(1)。写的时候务必多开一位让pre[0] 0这样pre[r1] - pre[l]就是闭区间[l, r]的和不用特判l 0。def build_prefix(a): pre [0] * (len(a) 1) for i, v in enumerate(a): pre[i 1] pre[i] v return pre # 闭区间 [l, r] 的和0-indexed def range_sum(pre, l, r): return pre[r 1] - pre[l]二维前缀和稍微绕一点但套路一样pre[i1][j1] pre[i][j1] pre[i1][j] - pre[i][j] a[i][j]求子矩阵时用容斥四块加减。这个公式我建议直接背下来因为推导一遍要花不少时间。差分是前缀和的逆操作用来处理给区间每个数加同一个值这种批量修改。做法是在diff[l] v、diff[r1] - v最后对 diff 求一次前缀和就还原了。差分 前缀和这一对组合几乎能秒掉所有区间修改 单点查询的题。3.4 双指针与滑动窗口双指针分两类同向的双指针快慢指针、滑动窗口和相向的双指针两数之和、有序数组。滑动窗口的通用骨架我写成一个固定形状遇到最长/最短满足条件的子数组就往里套def longest_window(a, ok): left 0 best 0 for right in range(len(a)): # 把 a[right] 加入窗口 while not ok(a, left, right): # 从左边收缩直到窗口重新合法 left 1 best max(best, right - left 1) return best这里的关键在于ok函数的判断要能 O(1) 完成通常靠一个计数器或哈希表来维护窗口内的状态。如果你在ok里又循环扫一遍窗口那复杂度就退化成 O(n²) 了这就白写了。4. 数据结构模板栈、堆、并查集与哈希Python 内置的数据结构已经很强但有几个地方的用法差异会导致性能天差地别这一节专门讲这些看起来一样其实不一样的地方。4.1 栈与队列list 和 deque 的分工栈就用listappend和pop都是 O(1)没有任何问题。队列就不一样了。很多人图省事也用list然后list.pop(0)出队——这一步是 O(n)因为它要把后面所有元素往前挪。队列操作 1e5 次就是 1e10 级别的工作量直接超时。正确做法是from collections import deque用append和popleft两个都是 O(1)。from collections import deque q deque() q.append(1) q.append(2) x q.popleft() # O(1)不要用 list.pop(0)顺便说一句deque还能当双端队列用appendleft、pop也能通过maxlen参数做成固定长度的滑动窗口用起来很舒服。4.2 heapq 与堆的常见坑heapq是最小堆想要最大堆就把元素取负存进去。这里有三个我踩过的坑第一个坑是**heapq里的元素如果是元组会比较到第二个元素**。如果你只想按第一个字段排序、第二个字段是对象可能就会TypeError: not supported。解决办法是在元组后面加一个自增的计数器做 tie-breaker(priority, counter, item)。第二个坑是**heapq不保证稳定**同样的优先级出堆顺序不确定别指望它按插入顺序。第三个坑是**heapq没有decrease_key**。Dijkstra 里想更新某个点的距离标准做法是直接再 push 一个新条目出堆时判断这条是不是过期的记一个dist数组比较一下过期就跳过。这叫懒惰删除虽然堆里条目多了但总复杂度还是 O(m log m)可以接受。import heapq def dijkstra(n, adj, src): INF float(inf) dist [INF] * (n 1) dist[src] 0 pq [(0, src)] while pq: d, u heapq.heappop(pq) if d dist[u]: continue # 过期条目跳过 for v, w in adj[u]: nd d w if nd dist[v]: dist[v] nd heapq.heappush(pq, (nd, v)) return dist4.3 并查集路径压缩加按秩合并并查集的模板必须背到能默写因为蚂蚁感冒出差这类题背后经常藏着连通性问题。标准写法是路径压缩 按大小合并两个优化一起用单次操作接近 O(1)。class DSU: def __init__(self, n): self.p list(range(n)) self.sz [1] * n def find(self, x): p self.p root x while p[root] ! root: # 先找根 root p[root] while p[x] ! root: # 再把路径上的点直接挂到根上 p[x], x root, p[x] return root def union(self, a, b): ra, rb self.find(a), self.find(b) if ra rb: return False if self.sz[ra] self.sz[rb]: ra, rb rb, ra self.p[rb] ra self.sz[ra] self.sz[rb] return True我把find写成迭代而不是递归就是为了避开递归深度限制。递归版大概四行看着清爽但碰上退化链的时候会炸栈赛场上不值得冒这个险。4.4 哈希表与 Counter 的取舍dict和collections.Counter是 Python 的杀手锏。计数、去重、分组一行就能搞定。但要注意两点一是**Counter的most_common(k)内部是排序**复杂度 O(n log n)如果你只是想要最大值用max(cnt, keycnt.get)更快。二是哈希的键最好是不可变类型元组可以列表不行。做网格类搜索时把坐标(x, y)直接当键存 visited比给每个点编号再转成一维省事得多。5. 搜索与图论模板BFS、DFS 与最短路搜索类题目在蓝桥杯里出现的频率非常高尤其是省赛阶段网格 BFS 几乎是送分题的定位——但前提是你得写得出来。5.1 网格 BFS 的通用骨架我把网格 BFS 抽象成一个固定骨架只要改check判断和终点条件就能复用from collections import deque def bfs(grid, start, is_target): n, m len(grid), len(grid[0]) dist [[-1] * m for _ in range(n)] sx, sy start dist[sx][sy] 0 q deque([start]) dirs ((1, 0), (-1, 0), (0, 1), (0, -1)) while q: x, y q.popleft() if is_target(x, y): return dist[x][y] for dx, dy in dirs: nx, ny x dx, y dy if 0 nx n and 0 ny m and dist[nx][ny] -1 \ and grid[nx][ny] ! #: dist[nx][ny] dist[x][y] 1 q.append((nx, ny)) return -1几个细节值得说dist数组同时承担是否访问过的职责用 -1 表示未访问省掉一个 visited 数组方向的写法用元组循环比手写四个 if 更不容易漏边界判断写成0 nx n这是 Python 特有的链式比较比nx 0 and nx n顺眼。如果题目要求所有可能的路径数或者八方向连通块只需要改dirs和dist的更新逻辑。如果是 BFS 求最短步数且边权相同上面就是标准解边权不同就得上 Dijkstra。5.2 DFS 与回溯DFS 我一般分两种用法。一种是只要遍历不需要撤销状态这种就用栈模拟或者直接递归。另一种是要枚举所有方案那就得写回溯路口选择、进入、撤销、退出四步齐全def backtrack(path, used, n, res): if len(path) n: res.append(path[:]) # 一定要拷贝否则存的是引用 return for i in range(n): if used[i]: continue used[i] True path.append(i) backtrack(path, used, n, res) path.pop() # 撤销 used[i] False # 撤销这里最容易出的错就是res.append(path)而不是res.append(path[:])。因为path是同一个列表对象你后面还会改它最后 res 里全是空列表或者同一个终态。我第一次写全排列就栽在这调了半小时才发现。回溯的剪枝也很关键。排列组合类问题如果某条分支已经不可能产生更优解就提前 return。剪枝写得好能把指数级搜索压到能过测试点的规模。5.3 Dijkstra 与 Floyd 的选择两个最短路算法什么时候用哪个我总结了三条判断场景选谁理由单源、边权非负、稀疏图Dijkstra 堆O(m log m)最优多源、节点数 ≤ 400FloydO(n³) 但写起来最短边权有负Floyd 或 Bellman-FordDijkstra 会失效蓝桥杯里 Floyd 的出镜率其实挺高因为很多题的节点数只有一两百三重循环 800 万次在 Python 里大概一两秒能过。Floyd 的代码只有五行性价比极高def floyd(n, dist): for k in range(n): dk dist[k] for i in range(n): di dist[i] dik di[k] if dik INF: continue for j in range(n): nd dik dk[j] if nd di[j]: di[j] nd注意if dik INF: continue这一句它跳过了一大堆无效的加法在稀疏图上能省掉很大一部分时间。6. 动态规划模板背包三件套与记忆化DP 是区分省二和省一的分水岭也是模板价值最高的地方。因为 DP 的状态定义和转移方程千变万化但背包这一家子的代码骨架几乎不变。6.1 01 背包01 背包的关键在于内层循环从大到小遍历容量。为什么因为如果从小到大同一个物品可能在dp[j - w]里已经被放进去过一次了就变成了无限次那就不是 01 背包而是完全背包了。这个倒序的细节是 01 背包唯一容易错的地方。def knapsack_01(weights, values, cap): dp [0] * (cap 1) for w, v in zip(weights, values): for j in range(cap, w - 1, -1): # 倒序保证每个物品只用一次 if dp[j - w] v dp[j]: dp[j] dp[j - w] v return dp[cap]6.2 完全背包完全背包把内层换成从小到大因为物品可以用无限次def knapsack_full(weights, values, cap): dp [0] * (cap 1) for w, v in zip(weights, values): for j in range(w, cap 1): # 正序 if dp[j - w] v dp[j]: dp[j] dp[j - w] v return dp[cap]顺带说个判据如果题目里物品数量没给或者可以取任意多个那就是完全背包如果明说每个物品只能用一次那就是 01 背包。看清这一点直接决定循环方向。6.3 记忆化搜索想不出递推时的救命稻草有些 DP 的状态转移不是按顺序来的比如从某个点出发能拿到的最大收益这时用记忆化搜索比硬凑递推更省脑子。写法就是加个lru_cachefrom functools import lru_cache lru_cache(maxsizeNone) def f(i, j): if i 0: return 0 ...用lru_cache有个前提参数必须是可哈希的不能传列表。如果状态里有数组就得手动转成元组再传。另外递归深度也要提前放宽不然状态一深就炸。提醒lru_cache的开销比手写数组大一些状态数量在 1e6 以下还算能用超过这个量级就老老实实写成递推数组。竞赛里我用它的原则是能过就行先拿分再优化。7. 数论与数学模板数论题的特点是知道模板就是几行不知道就是一片空白。蓝桥杯填空题里经常出现第 10000 个质数依次排列后第 2024 位数字这类本质都是数论工具题。7.1 质数筛与质因数分解筛法我用埃氏筛的平方根优化版写法短、够用def sieve(n): is_p bytearray([1]) * (n 1) is_p[0] is_p[1] 0 i 2 while i * i n: if is_p[i]: is_p[i * i:: i] bytearray(len(is_p[i * i:: i])) i 1 return is_p这里用了切片的批量赋值把整段非质数一次性置 0比逐个循环快很多。bytearray也比list[bool]省内存1e7 的筛在内存上完全没压力。单次质因数分解用试除法就够只要试到i * i ndef factorize(n): res [] i 2 while i * i n: while n % i 0: res.append(i) n // i i 1 if n 1: res.append(n) return res7.2 GCD、快速幂与逆元math.gcd是 C 实现直接用别手写欧几里得。快速幂是常用件取模版本一定要记牢def fast_pow(base, exp, mod): res 1 base % mod while exp: if exp 1: res res * base % mod base base * base % mod exp 1 return res顺便说一句Python 的pow(a, b, m)三参数版本就是内置快速幂直接调用比手写还快因为它是 C 层的。但在需要理解原理或者做变形的题里手写版本还是得会。7.3 组合数与大整数Python 的大整数是天然的math.comb(n, k)直接算组合数不用担心溢出。这在填空题里特别舒服——C 选手还在推模运算你已经把答案打印出来了。但要注意如果题目要求取模后的结果就不能用math.comb了因为算出来的大数再取模中间过程的规模可能爆炸。这时候要用杨辉三角或阶乘 逆元。杨辉三角适合 n 比较小几百以内的情况写法就是两重循环C[i][j] C[i-1][j-1] C[i-1][j]。8. 实战经验与问题排查模板会背不等于能拿分赛场上真正拉开差距的是调试速度和心态管理。这一节我把这些年踩过的坑整理成可以直接查的东西。8.1 常见报错速查表报错信息大概率原因处理方式RecursionErrorDFS/BFS 递归太深开头加sys.setrecursionlimit或改迭代IndexError数组越界常见于前缀和没多开一位检查pre长度是不是n1ValueError: too many values to unpack每行元素个数和预期不符改用data[idx]游标法MemoryError二维数组开太大用一维化或bytearray输出inf或-1最短路没连通或初始化不对检查 INF 的初值和起点KeyError用了defaultdict之外的方式访问不存在的键改用dict.get(k, 0)结果是对的但超时循环里有 O(n) 操作被套进 O(n) 循环上前缀和、Counter、双指针这张表我每次赛前都看一遍因为报错本身不可怕可怕的是你不知道它为什么报。8.2 用对拍和小数据验证代替盲目提交蓝桥杯的提交次数是有限的所以每道题提交前我都会做两件事一是用手算的小数据对一遍比如 n 3 的情况人肉推一遍答案看代码输出是否一致二是写一个暴力解法对拍。暴力解法通常就是三重循环枚举几行代码但能帮你验证复杂解法的正确性。对拍的思路很朴素随机生成 100 组小数据跑一遍暴力、跑一遍优化版结果不一致就说明优化版有问题。这个过程在本地花五分钟能省掉赛场上一次提交机会。8.3 时间分配先拿满简单分我自己的时间策略是这样的开考后前十分钟把整张卷子扫一遍标出送分题常规题硬骨头三类。然后先做送分题和常规题填空题的答案能用暴力算的坚决用暴力因为这些题只看答案不看代码效率。剩下大概三分之一时间再啃硬骨头。这时候模板的价值就体现出来了——你不需要重新设计算法结构只需要把模板粘出来改状态定义和转移逻辑。省下的每一分钟思考时间都是在给你最后一次机会做压轴题。8.4 模板怎么整理才用得顺手最后说个方法论。我整理模板不用任何花哨的工具就是一个纯文本文件加清晰的注释头。每个模板前面写三行注释这段代码解决什么问题、时间复杂度是多少、哪些地方需要改。第三行最重要因为赛场上你需要的不是理解它而是知道改哪里。每隔一段时间我会把官方题库里的历年真题重做一遍凡是卡壳超过十分钟的题就把当时缺失的模板补进去。这么滚了两年模板文件大概稳定在三十来个片段覆盖了绝大多数题型。剩下的题基本就是这些片段的组合和变形了。我个人在实际操作中的体会是模板不是越全越好而是越熟越好。你有三十个能默写的模板远比有一百个需要翻查的模板管用。所以我建议你把上面这些代码亲手敲一遍敲的过程中不要复制粘贴让自己对每个边界条件形成肌肉记忆——考场上那几十分钟靠的就是这个。