第一次打蓝桥杯的省赛,我因为一个看起来很蠢的原因丢了两道题的分:用input()逐行读入一组十万量级的数据,本地跑着没问题,提交上去直接超时。那之后我才认真整理了一套自己的 Python 模板,赛前半小时翻一遍,赛场上直接复制粘贴改逻辑。这篇文章就是把我这几年攒下来的东西摊开讲清楚——**蓝桥杯必备模板(python蓝桥杯)**到底该包含哪些内容,每块模板背后的原理是什么,哪些坑我踩过,哪些写法看着优雅但实际会被卡常数。不管你是刚学完 Python 语法准备第一次参赛,还是打过一两届想冲省一,这篇都能直接拿去用。
需要先说明一点:下面所有模板都是我个人在实战里反复用过的版本,不是从某本教材里抄来的标准答案。蓝桥杯 Python 组的评测环境、时限设置和题目风格,决定了有些在别处通用的写法在这里并不好用,我会具体指出哪些地方需要改。
1. 蓝桥杯Python选手的模板库该长什么样
1.1 先搞清楚Python在这场考试里吃亏在哪
蓝桥杯的题目时限通常是按 C/C++ 的1秒或2秒来设的,同样的算法,Python 的常数开销可能是 C++ 的二十到五十倍。这意味着一个在 C++ 里跑 0.3 秒的算法,换成纯 Python 实现有可能逼近或超过时限。所以模板库的第一目标不是"写得漂亮",而是在算法正确的前提下把常数压到最低。
具体体现在几个地方。第一是输入规模大的题目,必须用sys.stdin而不是input(),前者少了每次调用时的 prompt 处理和类型转换开销。第二是循环体内的重复计算要提前提到循环外,比如len(a)、range(n)这类表达式,在十万级循环里累计起来很可观。第三是能用内置函数(sum、max、sorted、bisect)解决的,不要自己写 Python 层的循环,因为内置函数是 C 实现的,速度快一个量级。
很多人会问,那是不是要放弃 Python 转 C++?我的看法是没必要。蓝桥杯的题目难度分布是阶梯式的,填空题和前面几道编程题用 Python 完全吃得下,真正卡 Python 的往往是最后那道压轴题。把模板整理好,保证前面该拿的分一分不丢,性价比比临时换语言高得多。
1.2 模板不是越多越好:我用的三层清单法
我见过有人整理了上百页的模板,赛场上翻都翻不完,反而浪费时间。我的做法是把模板分成三层,按出场频率组织。
| 层级 | 内容 | 使用频率 | 赛前处理方式 |
|---|---|---|---|
| 第一层 | 读入输出、二分、前缀和、DFS/BFS、并查集 | 几乎每题都用 | 必须背到能默写 |
| 第二层 | 01背包、完全背包、Dijkstra、素数筛、快速幂 | 经常出现 | 记住框架和关键判断 |
| 第三层 | 区间DP、树形DP、LCA、线段树 | 偶尔出现 | 知道思路,用时查笔记 |
这三层不是随便分的,而是按"模板记忆成本"和"题目出现概率"的比值来的。第一层的模板总共不到两百行代码,却覆盖了蓝桥杯里绝大多数题目的骨架,属于绝对要刻进肌肉记忆的部分。第三层的模板动辄几十行、边界条件多,赛场上现写容易出错,我更倾向于记住思路,真遇到了再去翻自己整理的代码片段。
提醒一句:模板要自己敲一遍再存进本地文件,不要直接复制别人的。手敲的过程会强迫你理解每个变量的含义,赛场上改起来才不会慌。
2. 读入输出:所有模板的第一块地基
2.1 sys.stdin 和 input 的真实差距
先看一个我实测过的对比。读入十万行、每行一个整数,两种写法的耗时大概是这样:
import sys, time # 写法一:input start = time.time() data = [int(input()) for _ in range(100000)] print("input:", time.time() - start) # 写法二:sys.stdin start = time.time() data = list(map(int, sys.stdin.read().split())) print("read:", time.time() - start)在普通笔记本上,第一种大约 0.05 秒,第二种大约 0.02 秒。这个差距看似不大,但当题目里还有一层十万次的循环时,输入部分省下来的时间就变成了宝贵的余量。更关键的是sys.stdin.read()一次性把整个输入读进来,不管数据是几行、有没有规律的空格分隔,split()之后都能拿到一个扁平的 token 列表,处理起来非常省心。
我常用的标准读入头是这样:
import sys input = sys.stdin.readline n = int(input()) a = list(map(int, input().split()))这里把input这个名字重新绑定到sys.stdin.readline,是个很实用的小技巧。原来的input()每次都会调用sys.stdout.write打印提示(虽然空提示没有输出,但函数调用链路长),而readline只是读一行。注意用了readline之后,读到的字符串末尾是带\n的,做int()时 Python 会自动忽略首尾空白,所以不用手动strip();但如果要用split()再拼接,就得注意这个换行符。
2.2 不定长、多行、矩阵数据的三种读法
蓝桥杯的输入格式并不总是规规矩矩的"第一行n,第二行n个数"。我总结了三类常见情况。
**第一类:整块读入。**数据规模大、格式不重要的时候,直接:
data = sys.stdin.read().split() n = int(data[0]) a = list(map(int, data[1:1+n]))这种写法的好处是不关心换行,前面几个数当参数,后面的当数据,非常灵活。
**第二类:逐行处理。**有明确行结构、每行独立处理的时候:
n = int(input()) for _ in range(n): x, y = map(int, input().split()) # 处理**第三类:矩阵读入。**二维网格图,比如迷宫、方格图:
n, m = map(int, input().split()) grid = [list(map(int, input().split())) for _ in range(n)]如果是字符网格(比如.和#组成的迷宫),就改成list(input().strip())。这里必须加.strip(),否则每行末尾的换行符会混进列表里,导致索引越界或者比较出错——这个坑我在练习赛里踩过不止一次。
2.3 输出用 join,不要循环 print
输出看似简单,但循环print是个隐形的性能杀手。每次print都会触发一次向 stdout 的写操作,数据量大时开销明显。标准做法是把结果收集进列表,最后一次性输出:
out = [] for x in result: out.append(str(x)) sys.stdout.write("\n".join(out))如果是每行输出多个数的情况,就用sys.stdout.write(" ".join(map(str, row)) + "\n")。我在模板文件里干脆把这两个都写成了小函数,需要用的时候直接调用。
还有一个细节:"\n".join()最后不会自动补换行,很多题目对末尾换行不敏感,但有些严格的评测会要求最后一行也有换行符。我一般会在最后手动加一个"\n",图个稳妥。
3. 二分、前缀和、差分这三件套
3.1 整数二分的两套写法与中点取整的坑
二分看着简单,但边界问题是初学者最容易翻车的地方,尤其是"求第一个满足条件的位置"和"求最后一个满足条件的位置"这两类问题,写法完全不同。
我固定使用下面这套模板,它对应 C++ 的lower_bound语义:
def lower_bound(a, x): lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] < x: lo = mid + 1 else: hi = mid return lo关键点是区间取左闭右开[lo, hi),中点向下取整,更新时hi = mid而不是mid - 1。这样写的好处是循环一定会结束,返回的lo就是第一个大于等于x的位置。
另一类"求最大值"的二分(比如最大化最小值、最小化最大值)用的是另一种框架:
def check(mid): # 判断 mid 是否可行 ... lo, hi = 0, 10**18 while lo < hi: mid = (lo + hi + 1) // 2 if check(mid): lo = mid else: hi = mid - 1注意这里的mid = (lo + hi + 1) // 2,是向上取整。如果不加这个+1,当lo和hi相邻时mid会等于lo,check为真时lo = mid相当于没动,直接死循环。这个坑我见过太多人在赛场上卡二十分钟,其实记住"求左边界向下取整,求最大可行解向上取整"这句话就够了。
3.2 前缀和与差分:一对反向操作
前缀和解决的是"区间和查询密集"的问题。一维模板:
pre = [0] * (n + 1) for i in range(n): pre[i+1] = pre[i] + a[i] # 查询区间 [l, r] 的和(0-indexed 转成 1-indexed) total = pre[r+1] - pre[l]差分是前缀和的逆运算,解决的是"区间修改密集、最后统一查询"的问题。给区间[l, r]整体加上v:
diff = [0] * (n + 2) diff[l] += v diff[r+1] -= v # 最后还原 cur = 0 for i in range(n): cur += diff[i] a[i] = cur两者的选择信号很清楚:**修改少、查询多,用前缀和;修改多、查询少(或只在最后查一次),用差分。**二维版本就是把这两个公式各自推广到四个角,二维前缀和的容斥公式是pre[i][j] = a[i][j] + pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1],二维差分的更新是四个点各加减一次。我在纸上推导过一遍之后就再没忘过,建议你也推一次,比死记强。
3.3 浮点二分:用迭代次数控制精度
浮点二分和整数二分不一样,不要用lo < hi判断,而是固定循环次数:
lo, hi = 0.0, 1e9 for _ in range(100): mid = (lo + hi) / 2 if check(mid): lo = mid else: hi = mid迭代一百次大约能把区间缩小到原来的 2 的负一百次方,精度远超题目要求(一般要求 1e-6 或 1e-8)。用固定次数而不是abs(hi - lo) > eps判断,可以避免浮点误差导致的死循环,也更省心。
4. 搜索:DFS回溯与BFS框架
4.1 DFS的"选择-递归-撤销"三段式
蓝桥杯里的搜索题大多是全排列、组合枚举、棋盘放置、连通块计数这几类。回溯型 DFS 的骨架非常固定:
def dfs(pos): if pos == n: # 到达终点,记录答案 return for choice in candidates: if not valid(choice): continue used[choice] = True path.append(choice) dfs(pos + 1) path.pop() used[choice] = False核心是做完选择后必须把状态恢复原样,否则后面的分支会看到被污染的状态。我见过的最典型错误是忘记path.pop(),导致答案里混进多余的数;或者忘记把used复位,导致某些数只被用一次。写的时候可以在心里念三个字:"选、递、撤",养成习惯就不会漏。
连通块计数(比如求岛屿数量)用的又是另一种 DFS,不需要撤销:
def dfs(x, y): if x < 0 or x >= n or y < 0 or y >= m or grid[x][y] != 1: return grid[x][y] = 0 for dx, dy in dirs: dfs(x + dx, y + dy)这里直接把访问过的格子标记为 0,相当于用原数组当访问标记,省掉一个visited数组。
4.2 BFS队列模板与层数记录
求最短步数的题目一律用 BFS,因为 BFS 第一次访问到某个状态时走的就是最短路径。标准模板:
from collections import deque def bfs(start): q = deque([start]) dist = {start: 0} while q: cur = q.popleft() if cur == target: return dist[cur] for nxt in neighbors(cur): if nxt not in dist: dist[nxt] = dist[cur] + 1 q.append(nxt) return -1用dict做dist的好处是状态可以是任意可哈希对象,比如坐标元组、字符串、甚至编码后的整数。如果状态是固定范围的小整数,用列表会更快;不确定的时候就用字典,稳。
需要按层处理(比如每层单独统计)的时候,可以在while里先记录for _ in range(len(q)),把当前队列里所有元素一次性处理完,这样dist里的值天然就是层号。
4.3 递归爆栈与剪枝的取舍
Python 默认递归深度只有一千左右,蓝桥杯里稍大一点的搜索就会报RecursionError。解决办法是在代码开头加:
import sys sys.setrecursionlimit(1 << 20)把它设到一百万,基本够用。但要注意,设得太高如果程序真的无限递归,可能会直接崩掉而不是抛异常,调试时要留个心眼。
剪枝是搜索题拉开差距的地方。最基本的两种:一是可行性剪枝,当前状态已经不可能到终点就返回;二是最优性剪枝,当前代价已经超过已知最优解就返回。我在处理"最少操作次数"的题目时,习惯先跑一次贪心或 BFS 得到一个上界,再用它去剪 DFS 的分支,很多时候能把指数级的枚举压到可接受的范围。
5. 动态规划:背包、记忆化与滚动数组
5.1 01背包倒序、完全背包正序的口诀
一维数组实现背包是必须背下来的:
# 01背包:每件物品最多选一次 dp = [0] * (W + 1) for i in range(n): w, v = items[i] for j in range(W, w - 1, -1): dp[j] = max(dp[j], dp[j - w] + v) # 完全背包:每件物品可以选无限次 dp = [0] * (W + 1) for i in range(n): w, v = items[i] for j in range(w, W + 1): dp[j] = max(dp[j], dp[j - w] + v)两者的唯一区别就是内层循环的方向。为什么 01背包要倒序?因为dp[j]依赖的是上一轮(还没放当前物品)的dp[j-w],倒序遍历时dp[j-w]还没被本轮更新过,正好对应"每件物品只用一次"。完全背包正序则允许同一件物品在dp[j-w]里已经放过,天然实现无限次选取。想明白这一层,就再也不用死记了。
5.2 记忆化搜索:@lru_cache 的正确用法与坑
对于状态转移复杂、递推顺序不直观的题目,记忆化搜索比硬写递推表更省脑力:
from functools import lru_cache @lru_cache(maxsize=None) def f(i, j): if i == 0: return 0 ...maxsize=None表示不限缓存大小。这里有两个坑要说。第一,参数必须是可哈希的,传列表进去会报错,需要先转成元组。第二,递归深度问题依然存在,记忆化搜索本质上还是递归,遇到深链状的状态转移记得先用setrecursionlimit抬高上限。
还有一个常见问题是缓存把内存吃满。蓝桥杯的题目内存限制一般是 256MB,如果状态空间特别大(比如参数范围都是几千),缓存条目可能上百万,那就老老实实开二维数组手写递推,别图省事。
5.3 二维压一维的时机判断
二维 DP 什么时候能压成一维,判断标准很简单:当前状态只依赖上一行的数据,而不依赖本行已经算出来的数据。背包问题符合这个条件,所以能压。但像区间 DP、树形 DP 这类需要同时访问同一层多个状态的,压不了。
压的时候要特别注意遍历顺序。以 01背包为例,压维之后内层必须倒序,原因上面已经说了。如果题目本身要求正序(比如某些计数类问题),说明它每件物品可以用多次,此时压维是合法的。搞不清楚的时候,先老老实实写二维版,跑通样例之后再尝试压维做优化,不要在没验证正确性的情况下直接写一维。
6. 数论、图论与那些能直接抄的偷懒工具
6.1 素数筛、GCD与快速幂
埃氏筛写起来最短,够用:
def sieve(n): is_prime = [True] * (n + 1) is_prime[0] = is_prime[1] = False for i in range(2, int(n ** 0.5) + 1): if is_prime[i]: for j in range(i * i, n + 1, i): is_prime[j] = False return is_prime如果数据范围到千万级、又要反复查素数,换成线性筛(欧拉筛),每个合数只被最小质因子标记一次。
最大公约数直接用内置的math.gcd,别自己写欧几里得,内置版本是 C 实现的。快速幂是用来处理"求 a 的 b 次方模 p"的:
def fast_pow(a, b, mod): res = 1 a %= mod while b: if b & 1: res = res * a % mod a = a * a % mod b >>= 1 return resPython 的内置pow(a, b, mod)已经实现了快速幂,速度还更快,所以标准库能用就用标准库,这个函数留着理解原理就好。
6.2 并查集与Dijkstra堆优化
并查集是图论题的万能胶水,模板如下:
parent = list(range(n + 1)) def find(x): while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x def union(a, b): ra, rb = find(a), find(b) if ra != rb: parent[ra] = rbfind里那句路径压缩是关键,它把查找路径上的节点直接指向祖父,均摊下来接近常数时间。写成迭代而不是递归,是为了避开递归深度限制。
Dijkstra 用堆优化:
import heapq def dijkstra(start, graph, n): dist = [float('inf')] * (n + 1) dist[start] = 0 pq = [(0, start)] while pq: d, u = heapq.heappop(pq) if d > dist[u]: continue for v, w in graph[u]: nd = d + w if nd < dist[v]: dist[v] = nd heapq.heappush(pq, (nd, v)) return distif d > dist[u]: continue这行是懒删除优化,堆里可能存着同一个节点的多个过期条目,直接跳过即可。没有这行代码虽然也正确,但常数会明显变大。
6.3 itertools:允许用就尽量用
蓝桥杯允许使用标准库,itertools里的函数能省下大量手写代码:
permutations(a, k):从a中取k个元素的排列combinations(a, k):组合product(a, repeat=k):笛卡尔积,适合多重循环展开accumulate(a):前缀和,等价于手写pre数组
填空题里枚举全排列时,permutations比自己写回溯快得多也短得多。但编程大题数据规模大的时候要慎用,因为它是生成器,每个元素都要在 Python 层迭代一次,常数不小。
6.4 日期题和模拟题的通用处理
日期类题目在蓝桥杯出现频率相当高。我习惯用一个固定的判断闰年函数加上datetime模块:
from datetime import date, timedelta def is_leap(y): return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) start = date(2000, 1, 1) end = date(2024, 12, 31) cur = start while cur <= end: # 处理 cur cur += timedelta(days=1)用datetime的好处是不用自己处理月份天数和闰年,timedelta自动进位。缺点是遍历跨度大的时候慢,比如从公元一年遍历到今天,几百万次循环在 Python 里要好几秒。这种时候要么用数学公式直接算,要么把范围缩小。我在赛场上遇到日期题,第一反应是先估算遍历量,超过百万级别就考虑换算法。
7. Python常数优化:把TLE变AC的几个动作
模板写得再对,如果常数压不下去,一样会被卡。这一节讲的是我在真实提交里验证过的几种提速手段。
**第一,局部变量比全局变量快。**在 Python 里访问局部变量走的是数组索引,全局变量走的是字典查找。把常用函数在主函数里重新赋值成局部变量,比如push = heapq.heappush,能带来可观的提速。这个技巧在循环调用密集的代码里效果明显。
第二,避免在循环里做重复的属性访问。a.append这类绑定方法如果在循环里反复写a.append(x),Python 每次都要重新查找append属性。改成push = a.append之后循环里只调用push(x),快不少。
第三,善用集合做存在性判断。x in list是线性扫描,x in set是哈希查找。数据量大时把需要频繁查询的容器换成set或dict,复杂度直接从 O(n) 降到 O(1)。
**第四,字符串拼接用列表收集再 join。**在循环里用s += t拼接字符串,Python 会不断创建新对象,复杂度接近 O(n²)。正确做法是把片段追加进列表,最后"".join(parts)。
第五,位运算代替部分算术。x // 2写成x >> 1,x % 2写成x & 1,在小整数上确实有提速,虽然幅度有限,但在循环体里累计起来还是值得的。
**第六,能用内置函数就绝不手写循环。**求最大值用max,求和用sum,排序用sorted,二分查找用bisect。这些函数底层是 C,比任何 Python 层的实现都快。
我在整理模板的过程中最大的体会是,模板的价值不在于让你少打字,而在于让你在赛场上不必分心去想"这个边界怎么写""这个循环方向对不对"。把这几百行东西敲熟、用熟,你就有了一个稳定的起点,剩下的精力可以全部投到题目逻辑本身。至于哪些模板适合放进你自己的库,我的建议是先照着上面这七块各写一遍,跑几道真题验证,用得别扭的就改掉,留下顺手的那些。模板这东西只有自己调教过才真正靠得住。