算法题常用技巧

前面几篇讲了 Python 的语法、函数和数据结构,已经够你看懂大部分代码了。这篇是 Python 教程的最后一篇,补充一些刷算法题时经常用到的实用技巧,都是一些零散但重要的知识点。

ACM 模式 I/O 模板

很多 OJ 平台(包括本站)使用 ACM 模式,需要你自己处理输入输出。前面在基础语法那篇其实已经讲过 input()split() 的用法了,这里再系统整理一下刷题中最常用的几种输入模板。

input().split() 基本用法

最基础的套路:input() 读一行,split() 按空格切分,map(int, ...) 批量转整数:

如果一行有很多数字需要存到列表里:

nums = list(map(int, input().split()))

这几个写法在基础语法那篇讲过,这里不再赘述。

sys.stdin 快读(大数据量)

当输入数据量很大(比如几十万行),input() 可能会超时。这时候需要用 sys.stdin 来加速读取。方法很简单,只需要在代码开头加一行:

就这么一行 input = sys.stdin.readline,把 Python 内置的 input 函数替换成更快的版本,后面的代码完全不用改。这是竞赛选手的标准操作。

注意sys.stdin.readline 读取的字符串末尾会带一个换行符 \n,但 split()int() 会自动处理掉,所以大部分情况不需要额外操心。只有在直接拿字符串比较的时候要注意用 strip() 去掉末尾的换行。

处理多组输入 / EOF

有些题目不告诉你有多少组输入,而是读到文件末尾(EOF)为止。这时候用 try-except 捕获异常。try 块中的代码如果出错,程序不会崩溃,而是跳到 except 块继续执行。这里利用这个机制检测输入结束:没有更多输入时读取会出错,被 except 捕获后执行 break 退出循环:

input() 读到 EOF 时,会返回空字符串,split() 得到空列表,map(int, ...) 拆包就会抛异常,被 except 捕获后 break 跳出循环。

还有一种更简洁的写法,直接用 sys.stdin 遍历每一行:

import sys
for line in sys.stdin:
    a, b = map(int, line.split())
    print(a + b)

sys.stdin 本身就是一个可迭代对象,读完所有行自动结束,连 try-except 都不用写。

排序

sorted() 和 list.sort() 的区别

Python 有两种排序方式:sorted() 返回一个新列表,原列表不变;list.sort() 原地排序,不返回新列表:

一般来说,需要保留原列表就用 sorted(),不需要保留就用 .sort(),后者省内存。

key 参数自定义排序

key 参数接收一个函数,告诉排序算法"用什么规则比较"。配合 lambda 表达式非常方便:

多级排序:元组比较的特性

Python 比较元组时是逐元素比较的:先比第一个,相等再比第二个,以此类推。利用这个特性可以轻松实现多级排序:

key=lambda x: (-x[1], x[0]) 返回的是一个元组 (-分数, 名字)。Python 会先比较 -分数(取负实现降序),相等再比较 名字(自然就是升序)。这个元组比较的技巧在刷题中非常常用。

cmp_to_key:自定义两两比较

大部分排序用 key 就够了,但有些场景需要两两比较才能定义顺序,比如经典的"最大数拼接"问题。这时候需要 cmp_to_key

cmp_to_key 把一个"比较函数"转换成 key 函数。比较函数接收两个参数 a, b,返回负数表示 a 排前面,正数表示 b 排前面,0 表示相等。

说实话,绝大部分排序题用 key + lambda 就能搞定,cmp_to_key 只有在"必须两两比较才能定义顺序"的时候才需要。

常用内置函数

Python 有很多内置函数可以直接用,不需要 import,刷题时非常方便。

max、min、sum、abs

max/min 带 key 参数

maxmin 也支持 key 参数,用来指定比较规则。这在刷题中非常实用:

divmod 和 pow

pow(a, b, mod) 这个三参数版本在算法题中非常有用。很多题目要求"答案对 10^9+7 取模",直接用三参数 pow 就行,底层用的是快速幂算法,效率很高。

math 模块

Python 的 math 模块提供了常用的数学函数和常量:

刷题中 math.ceilmath.gcdmath.inf 用得最多。不过要注意,Python 里表示无穷大还有另一种方式 float('inf'),两种都能用。

整数特性

Python 整数无溢出

这是 Python 最爽的特性之一:整数可以任意大,不会溢出! 不管你算多大的数,Python 都能精确表示:

这意味着你在 Python 里刷题永远不用担心整数溢出。不需要考虑 int32、int64 的范围问题,该怎么算就怎么算。

整除 // 和取模 % 的行为

Python 的整除 //向下取整(向负无穷方向),不是"向零取整"。在处理负数的时候要特别注意:

大部分刷题场景处理的是非负数,不会遇到这个问题。但如果题目涉及负数的整除或取模,要留个心眼。

float('inf') 和 float('-inf')

在算法中,我们经常需要一个"无穷大"或"无穷小"的初始值,比如求最小值时把初始值设为无穷大:

刷题中用 float('inf') 更常见一些,因为不需要 import,写起来方便。

进制转换

有些算法题会涉及进制相关的问题,了解标准库提供的进制转换函数,就不需要自己手搓转换逻辑了。Python 提供了几个内置函数,可以在十进制和其他进制之间互相转换:

刷题中最常用的是 bin()int(s, 2),比如位运算相关的题目经常需要在二进制和十进制之间转换。bin(n).count('1') 统计二进制中 1 的个数也是个常用技巧,虽然不是最高效的方式,但写起来很简洁。

实用工具

Counter:一行统计频率

Countercollections 模块提供的计数器,能一行代码统计元素出现的频率:

Counter 在刷题中非常常用。比如"判断两个字符串是否是字母异位词",只需要 Counter(s1) == Counter(s2) 一行就搞定了。

defaultdict:带默认值的字典

defaultdict 是一个增强版的字典,访问不存在的 key 时会自动创建默认值,省去了手动判断 key 是否存在的麻烦:

defaultdict(list) 在图论题目中建邻接表时特别好用。普通字典需要先判断 key 存不存在再 append,defaultdict 直接 append 就行。

defaultdict(int) 用来计数也很方便,不过如果只是简单计数,用 Counter 更直接。

bisect:二分查找

bisect 模块提供了在有序列表中进行二分查找和插入的函数:

bisect_leftbisect_right 的区别在于处理重复元素时:bisect_left 返回最左侧的插入位置,bisect_right 返回最右侧的。这跟我在算法教程中讲的"二分搜索左边界"和"二分搜索右边界"是一个意思,不了解的同学可以去看 二分搜索 那篇文章。

恭喜你!看完这四篇教程,你已经掌握了用 Python 刷算法题所需的全部知识。

Python 的优势就是简洁,很多操作一行就能搞定。现在你可以去刷题了,遇到不熟的语法随时回来翻这个教程就行。加油!