LeetCode Logo

分享|算法竞赛中 Python 的优化技巧

3357
2025.08.08
2026.07.28
发布于 福建
前言
读写
使用快读
一次性读入全部输入
暂存结果统一输出
数据类型
int
优化取模
统一取模
str
用 bytearray 做可变字符串
list
用迭代器替代按下标取值
预分配 list 空间
多维 list 把大维度放在内部
多维 list 下标映射到一维
使用切片
用 array.array 代替 list
用 bytearray 代替布尔数组
用 ctypes 的 C 数组代替 list
tuple
用数组结构代替结构数组
dict
用 list 代替 dict / set
用 dict.items 同时取键值
函数
用内置函数代替手写循环
带变换的归约用手写循环
手写 min/max
手写快速幂
手写 memo 代替 @cache
用 yield 逐个产出结果
用生成器代替列表推导式
把主程序封装进函数
类
把成员变量缓存到局部
用数组代替类实例
杂项
用栈模拟代替递归
用链式前向星代替邻接表
总结
把握正优化、避免负优化
两引擎各有所长
关于语言本身
参考
英文版本:CodeForces。
前言
本文旨在介绍在算法竞赛使用 Python 时,在复杂度正确且不变的前提下,能缩短程序运行时间的技巧。每条技巧均附有 CPython 3.11.5 / PyPy 3.10.13 下的实测加速比。
读写
使用快读
把内置 input 替换为 sys.stdin.readline。input() 每次调用都附带提示处理和 sys.stdin 查找开销,readline() 直接读取一行。输入行数越多差异越大。
实测加速:CPython 约 ×4.2;PyPy 约 ×16.4。
Python
import sys
input = lambda: sys.stdin.readline().rstrip()  # 需要删除行末换行符
II = lambda: int(input())
LII = lambda: list(map(int, input().split()))
一次性读入全部输入
用 sys.stdin.read().split() 一次读完,再按需取用。这样可以避免反复调用读取函数。
实测加速:CPython 约 ×1.7;PyPy 约 ×1.0,JIT 把逐行读取优化得与原写法相当。
Python
import sys
# 输入仅包含整数
it = map(int, sys.stdin.read().split())
II = lambda: next(it)
# 输入包含字符串
it = iter(sys.stdin.read().split())
SI = lambda: next(it)
II = lambda: int(SI())
暂存结果统一输出
把所有答案先存进一个列表,最后用 print(*output) 统一输出,而不是每算出一个就 print 一次,从而减少输出函数的调用次数。不过输出通常不是瓶颈。
实测加速:CPython 约 ×1.1;PyPy 约 ×1.0。
Python
output = []
for _ in range(n):
    ans = solve()
    output.append(ans)
print(*output, sep='\n')
数据类型
int
优化取模
整数取模本质是除法,远比加减乘开销大,因此对两个不超过 MOD 范围的整数做加减,可用条件分支代替取模。然而实测和这一猜测完全不符!% 是 C 层快速操作,而条件分支是 Python 字节码层、自带开销;PyPy 的 JIT 对直线 % 优化得更好,分支反而拖慢。封装后函数调用的开销更是不划算。
实测加速:CPython 约 ×1.0;PyPy 约 ×0.20。
修改前:
Python
def add(x, y): return (x + y) % MOD
def sub(x, y): return (x - y) % MOD
修改后:
Python
def add(x, y):
    x += y
    if x >= MOD: x -= MOD
    return x
def sub(x, y):
    x -= y
    if x < 0: x += MOD
    return x
统一取模
如果中间结果不会过大(例如不超过 long long 范围),不再每步计算都取模,只在最后取一次模。
实测加速:CPython 约 ×2.0;PyPy 约 ×3.3。
修改前:
Python
ans = 0
for i in range(n):
    ans = (ans + comb(n, i) * pow(2, i, MOD) % MOD) % MOD
修改后:
Python
ans = 0
for i in range(n):
    ans += comb(n, i) * pow(2, i, MOD)
ans %= MOD
str
用 bytearray 做可变字符串
用 bytearray(s, encoding) 创建类似 C++ 的可变字符串,原地修改后再 decode,从而避免 list(s) 改字符再 join 的往返。
实测加速:CPython 约 ×1.0;PyPy 约 ×1.75,仅在需要频繁原地改字符时有用。
修改前:
Python
t = list(s)
t[0] = 'a'
s = ''.join(t)
修改后:
Python
t = bytearray(s, encoding='ascii')
t[0] = ord('a')
s = t.decode('ascii')
list
用迭代器替代按下标取值
遍历容器时,尽量直接用迭代器取值,而不是按下标随机访问。能用内置函数(如 enumerate)或迭代器(如 reversed、zip 等)解决的,就不要写 for i in range(len(nums))。每次 nums[i] 都是一次独立的下标查询(字节码 BINARY_SUBSCR,带边界检查与整数拆箱),而直接迭代走迭代协议、顺序读取,更省字节码也更缓存友好。
实测加速:enumerate CPython 约 ×1.0、PyPy 约 ×1.2;reversed CPython 约 ×1.5、PyPy 约 ×1.5;zip CPython 约 ×1.2、PyPy 约 ×1.0。
修改前:
Python
for i in range(len(nums)):
    x = nums[i]
    ...
for i in range(len(nums) - 1, -1, -1):
    x = nums[i]
    ...
for i in range(len(a)):
    x, y = a[i], b[i]
    ...
修改后:
Python
for i, x in enumerate(nums):
    ...
for x in reversed(nums):
    ...
for x, y in zip(a, b):
    ...
预分配 list 空间
已知数组的最终长度时,先用 [0] * n 开好空间再按下标赋值,而不是从空列表 append。append 在容量不足时触发扩容与整体拷贝,预分配空间可以省去多次扩容。
实测加速:CPython 约 ×1.6、PyPy 约 ×5.0。
修改前:
Python
nums = []
for _ in range(n):
    nums.append(x)
修改后:
Python
nums = [0] * n
for i in range(n):
    nums[i] = x
多维 list 把大维度放在内部
构造多维 list 时,让较长的那一维成为内层 list,既减少创建 list 对象的数量,也改善内存连续性。
实测加速:CPython 约 ×1.05;PyPy 约 ×1.5。
修改前:
Python
n, k = 10**5, 20
dp = [[0] * k for _ in range(n)]
修改后:
Python
n, k = 10**5, 20
dp = [[0] * n for _ in range(k)]
多维 list 下标映射到一维
把多维数组拍平成一维数组,以二维数组为例,用 i * n + j 手动编解码下标。这样可以减少一层 list 间接寻址,内存连续。
实测加速:CPython 约 ×1.1;PyPy 约 ×1.1。
修改前:
Python
dp = [[0] * n for _ in range(m)]
修改后:
Python
dp = [0] * (m*n)
compress = lambda i, j: i*n+j
decompress = lambda k: divmod(k, n)
使用切片
对 list 的子数组进行操作时,直接切片出子数组再处理,而不是按下标逐个访问。切片在 C 层一次性拷出子数组,内置 sum / max 也在 C 层遍历,整体远快于 Python 字节码的下标循环。
实测加速:求 sum CPython 约 ×2.0、PyPy 约 ×0.72;求 max CPython 约 ×2.3、PyPy 约 ×1.8。注意在 PyPy 上,切片的开销可能胜过 JIT 对循环的优化。
修改前:
Python
s = sum(nums[i] for i in range(l, r))
mx = 0
for i in range(l, r): mx = max(mx, nums[i])
修改后:
Python
s = sum(nums[l:r])
mx = max(nums[l:r])
用 array.array 代替 list
把元素均为整数、定长、需频繁访问修改的 list 换成 array.array('i', ...)。array 在内存中紧凑存放 C 整数,省内存。然而,每次访问 array 时都要做 Python int 与 C 整数之间的转换,开销可能高于 list 自身。
实测加速:CPython 约 ×0.58;PyPy 约 ×0.73。说明这一做法只在内存占用情景下成立,在高频随机访问情景下不成立。
修改前:
Python
nums = [0] * n
修改后:
Python
from array import array
nums = array('i', [0] * n)
用 bytearray 代替布尔数组
把 [False] * n 换成 bytearray(n)。布尔数组仍是 list,相比之下 bytearray 每字节一个元素,紧凑。
实测加速:CPython 约 ×1.7;PyPy 约 ×0.9。PyPy 的 list 已高度优化,bytearray 反而更慢。
修改前:
Python
vis = [False] * n
修改后:
Python
vis = bytearray(bytes(n))
用 ctypes 的 C 数组代替 list
用 ctypes 的 c_int32 数组代替 list。ctypes 数组每次索引都是一次 C 调用,开销巨大。
实测加速:CPython 约 ×0.65;PyPy 约 ×0.003。这一方法只适合与 C 库互操作,绝不适合做高频访问容器。
修改前:
Python
rank = [0] * n
pa = list(range(n))
修改后:
Python
from ctypes import c_int32
rank = (c_int32 * n)()
pa = (c_int32 * n)(*range(n))
tuple
用数组结构代替结构数组
不要把多个字段组合成 tuple 再放进 list,而是用多个 list 分别保存各字段。
实测加速:CPython 约 ×0.75;PyPy 约 ×1.5。
修改前:
Python
items = [(w1, v1), (w2, v2), ...]
修改后:
Python
weights = [w1, w2, ...]
values = [v1, v2, ...]
dict
用 list 代替 dict / set
数据范围允许时,用下标访问的 list 代替按键访问的 dict / set。哈希表按键访问要先 hash、存取时可能触发 resize,远不如数组下标直接。
实测加速:CPython 约 ×1.8;PyPy 约 ×1.6。
修改前:
Python
g = defaultdict(list)
修改后:
Python
g = [[] for _ in range(n)]
用 dict.items 同时取键值
遍历字典同时要键和值时,用 for k, v in mp.items() 而不是 for k in mp: v = mp[k]。后者每次需要额外做一次哈希查找。
实测加速:CPython 约 ×1.2;PyPy 约 ×2.9。
修改前:
Python
for k in mp:
    v = mp[k]
    ...
修改后:
Python
for k, v in mp.items():
    ...
用 get 访问可能缺失的键
对 defaultdict,查询可能缺失的键时用 mp.get(k, default),而不是 mp[k]。mp[k] 在键缺失时会触发 __missing__ 插入一个新条目;get 只读不插。查询中缺失键占比高时差异明显。
实测加速:CPython 约 ×1.3;PyPy 约 ×1.35。
修改前:
Python
mp = defaultdict(int)
x = mp[k]
修改后:
Python
mp = defaultdict(int)
x = mp.get(k, 0)
函数
用内置函数代替手写循环
求最大值、求和等归约操作,直接用内置函数 max / sum,不要手写循环。内置函数在 C 层遍历,远快于 Python 字节码循环。
实测加速:max CPython 约 ×1.4、PyPy 约 ×2.1;sum CPython 约 ×1.5、PyPy 约 ×1.0,PyPy 的 JIT 把手写求和循环优化到与内置持平。带变换的归约是例外,见下一条。
修改前:
Python
mx = 0
for x in nums:
    if x > mx: mx = x
修改后:
Python
mx = max(nums)
带变换的归约用手写循环
求带变换或带过滤的归约时,用手写循环而不要用内置函数,如 sum(x*x for x in nums) 或 sum(x for x in nums if cond) 。其原因是:生成器逐元素走 yield 协议,每个元素都要在生成器与 sum 之间往返一次,开销大于一个紧凑的手写循环。
实测加速:求平方和 CPython 约 ×1.35、PyPy 约 ×1.14;带 if 过滤求和 CPython 约 ×1.18、PyPy 约 ×2.30。
修改前:
Python
s = sum(x for x in nums if x & 1)
修改后:
Python
s = 0
for x in nums:
    if x & 1:
        s += x
手写 min/max
只比较两个数时,用 x if x < y else y 代替 min(x, y)。max 同理。由于内置 min/max 要处理可变参数和类型检查,有额外开销,因此猜测手写会更快。
实测加速:CPython 约 ×1.9;PyPy 约 ×0.83,实际上 JIT 把内置 min/max 优化得比 lambda 调用还快。
修改前:
Python
x, y = min(x, y), max(x, y)
修改后:
Python
fmin = lambda x, y: x if x < y else y
fmax = lambda x, y: x if x > y else y
x, y = fmin(x, y), fmax(x, y)
手写快速幂
需要带模快速幂时,手写快速幂而不是用 pow(x, -1, MOD)(或 pow(x, MOD-2, MOD))。内置 pow 的扩展欧几里得逆元路径可能较慢。
实测结果:CPython 约 ×0.20,因为 pow(x, -1, MOD) 都是单次 C 调用,远快于 Python 循环;PyPy 约 ×1.8。
修改前:
Python
MOD = 10**9+7
inv = pow(x, -1, MOD)
修改后:
Python
MOD = 10**9+7
def qpow(x, k):
    res = 1
    while k:
        if k & 1:
            res = res * x % MOD
        x = x * x % MOD
        k >>= 1
    return res
inv = qpow(x, MOD-2)
手写 memo 代替 @cache
记忆化搜索不要用 functools.cache,改用显式的 memo 数组。@cache 要把参数打包成 tuple 再 hash,并用哈希表加双向链表(LRU)存结果,开销较大;手写数组可以直接按下标访问。
实测加速:CPython 约 ×1.17、PyPy 约 ×2.6。
修改前:
Python
@cache
def dp(i: int, pre: int, islim: bool, isnum: bool) -> int:
    ...
修改后:
Python
memo = [[-1] * 10 for _ in range(n)]
def dp(i: int, pre: int, islim: bool, isnum: bool) -> int:
    if not islim and not isnum and memo[i][pre] != -1:
        return memo[i][pre]
    ...
用 yield 逐个产出结果
把先收集结果再统一返回的函数改成用 yield 逐个产出的生成器,从而省去中间列表的分配。
实测加速:CPython 约 ×1.2;PyPy 约 ×14。
修改前:
Python
def all_subsets(mask):
    subs = []
    cur = mask
    while cur:
        subs.append(cur)
        cur = (cur - 1) & mask
    return subs
修改后:
Python
def all_subsets(mask):
    cur = mask
    while cur:
        yield cur
        cur = (cur - 1) & mask
用生成器代替列表推导式
接受可迭代对象的函数,直接传生成器表达式,不必先建 list,从而省去中间列表的分配。
实测加速:CPython 约 ×1.13;PyPy 约 ×0.48,可能是因为 list 推导式加 sum 走快路径,生成器反而更慢。
修改前:
Python
s = sum([x**2 for x in range(n)])
修改后:
Python
s = sum(x**2 for x in range(n))
把主程序封装进函数
把主程序逻辑写进 main() 再调用,使变量成为局部变量。局部变量用 LOAD_FAST,全局变量用 LOAD_GLOBAL,前者略快。
实测加速:CPython 约 ×1.1;PyPy 约 ×1.0。
修改:
Python
def main():
    ...
main()
类
用 slots 声明成员变量
在类里加 __slots__ 列出成员变量名,省去每个实例的 __dict__,属性访问更快、内存更省。
实测加速:CPython 约 ×1.18;PyPy 约 ×1.43。
修改前:
Python
class DSU:
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.size = [1] * n
修改后:
Python
class DSU:
    __slots__ = 'parent', 'size'
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.size = [1] * n
把成员变量缓存到局部
方法内频繁访问的 self.x,先赋给一个局部变量再用,前者是 LOAD_ATTR,后者是 LOAD_FAST。
实测加速:CPython 约 ×1.56;PyPy 约 ×1.0。
修改前:
Python
class DSU:
    def find(self, u: int):
        while u != self.parent[u]:
            u = self.parent[u]
        return u
修改后:
Python
class DSU:
    def find(self, u: int):
        parent = self.parent
        while u != parent[u]:
            u = parent[u]
        return u
用数组代替类实例
用数组代替类实例,如字典树、线段树等。类实例创建和属性访问都较慢。
实测加速:CPython 约 ×0.97;PyPy 约 ×1.83。
修改前:
Python
class TrieNode:
    def __init__(self):
        self.children = [None] * 26
        self.isend = False
        self.cnt = 0
class Trie:
    def __init__(self):
        self.root = TrieNode()
    ...
修改后:
Python
class StaticTrie:
    def __init__(self, lengths):
        lengths += 1
        self.children = [[-1] * lengths for _ in range(26)]
        self.isend = [False] * lengths
        self.cnt = [0] * lengths
        self.ptr = 1
    ...
杂项
用栈模拟代替递归
函数递归较深时,用显式栈模拟递归,或用无限递归装饰器[^1];sys.setrecursionlimit 不完全可靠。原因是函数调用帧有开销,且过深递归会栈溢出、引发 RE 或 MLE。以下是对树的先序遍历,而后序遍历仅需反转先序遍历结果即可。
实测加速:CPython 约 ×0.85;PyPy 约 ×1.36。
修改前:
Python
order = []
def dfs(u: int, pa: int):
    order.append(u)
    for v in tree[u]:
        if v != pa:
            dfs(v, u)
修改后:
Python
order = []
parents = [-1] * len(tree)
stk = [root]
while stk:
    u = stk.pop()
    order.append(u)
    for v in g[u]:
        if parents[u] != v:
            parents[v] = u
            stk.append(v)
用链式前向星代替邻接表
建图时不用 g = [[] for _ in range(n)] 和 append,而用链式前向星 head / to / weight / nxt 四个定长数组加头插法。
实测加速:CPython 约 ×2.6;PyPy 约 ×7.9。
修改前:
Python
g = [[] for _ in range(n)]
def add_edge(u: int, v: int, w: int):
    g[u].append((v, w))
修改后:
Python
head = [-1] * n
to = [-1] * m
weight = [0] * m
nxt = [-1] * m
ptr = 0
def add_edge(u: int, v: int, w: int):
    nonlocal ptr
    to[ptr] = v
    weight[ptr] = w
    nxt[ptr] = head[u]
    head[u] = ptr
    ptr += 1
总结
把握正优化、避免负优化
文章中大部分技巧在 CPython 和 PyPy 至少一边上都显著有效,减少对象分配、减少 Python 字节码执行量的优化主旨对两引擎都有益。相反地,有一些想当然的优化,实测两引擎都不成立或更慢,核心原因是,Python 字节码层的大头是分支、函数调用、属性访问,任何增加这些的所谓优化技巧往往得不偿失。
两引擎各有所长
CPython 没有热循环优化,逐条解释字节码,因此凡是能把工作一次性下推到 C 层面的写法都占优。PyPy 有 JIT,会把紧凑、类型稳定的 Python 循环编译成接近机器码的速度。因此:用手写循环或朴素容器代替 C 内置的技巧,CPython 上往往无效甚至更慢,PyPy 上可能更快;反过来,依赖 C 内置或紧凑结构的技巧,CPython 受益,PyPy 上常因为其他开销而更慢。
关于语言本身
Python 自身性质决定了其无论如何优化,都无法通过数据范围较大、复杂度较高的题目。此外,用 Python 写完代码再额外优化的时间,并不见得比直接用 C++ 快。因此尽管 Python 有一些优秀的语言特性,但如果想从事专业算法竞赛,还是放弃对 Python 的执念、多学一门 C++ 为好。
参考
评论 (12)