Python算竞教程
Python算竞教程
xiaoyan如何用Python参加算法竞赛
前言
本文适合有一定c++基础且初步了解Python,并想开发自己第二竞赛用语言的人群阅读。
本文仅介绍Python3,更低版本Python请自行了解。
Python的优点在于在应对代码编写简单的题目时,在无电子板子的赛场环境可以一定缩短codeing时间。但在面对代码编写要求较高、时间限制较紧的情况,并**无法取代c++**。因此c++仍然是打算法竞赛的第一选择。
Python的适用场合有如下几种:
- 代码简单的,如一些思维题、构造题等
- 字符串处理,Python提供的字符串处理函数比c++丰富许多。
- 对拍器和数据生成器
注一:
python与其他语言不同的一点在于,同样的算法,用标准库的永远比自己写的速度快。因为标准库算法大部分是用C语言写的,python由于语言限制永远到达不了C的速度,也即标准库的速度。
注二:
python的官方中文文档比起一些别的语言已经算非常好了,如果看别人代码或者题解有不懂的函数或容器,可以直接搜官方文档的对应内容。本文对于一些内容也不会讲太细,可以直接搜官方文档看
注三:
python语言并不适合递归算法,因为其递归深度,语言自身就有限制,就算去除限制,其也会开辟大量空间。蓝桥杯也会依据语言出题,python组用递归算法的题很少很少。所以平时应多注意迭代算法的积累,以及递归算法转迭代算法的方式
Python基本数据类型
python的基本数据类型有六种,数值、字符串、列表、元组、字典、集合,常用的有int,str,bool,float,list,tuple,dict,set
int(整数)
没有长度限制,大数字乘法复杂度O(nlogn)O(nlogn)(补:因为当int达到高精度时,内部会使用FFT算法加速多项式乘法),非常方便。
float(浮点数)
大概注意一下精度就行,[2.2250738585072014e−308,1.7976931348623157e+308][2.2250738585072014e−308,1.7976931348623157e+308]
bool(布尔)
有True
,False
两个值(注意大小写)
list(列表)
最常用的数据类型之一,可当成C++中数组。
由于python中没有C++的栈,该结构可作栈使用
1 | a = list(map(int, input().split()))#将读入创建为一个整数list |
还可以append添加元素,pop删除尾部最后一个元素(O(1)),删除中间元素(最坏O(n)),count计数(O(n)),index定位元素(O(n)),并且重载了加法(即同维数组连接)
加法示例
1 | a = [1, 2, 3] |
tuple(元组)
和list差不多,初始化用括号
1 | a = (1, 2, 3) |
支持list的很多操作,唯独不能对一个tuple自身进行修改
所以 dict 因为要求 key 值不可变,当想对插入一个 (list,int)键值对时,必须将 list 转为 tuple
str(字符串)
和 C++ 中字符串类似,但是无法修改其中字符,因此经常用如下方法转换为一个list再进行操作。
1 | s = '1323' |
重载了加法,加法即是字符串连接
同时也有count,index,find等函数
多了一个C++没有,很好用的 split
split
默认以不可见字符进行分割,也可传入固定字符,以固定字符进行分割字符串,将子串存入list中
1 | s = 'hello my name is caidd!!!' |
dict(字典)
相当于C++的map容器,但是其内部是哈希表实现的,无序,大部分操作都是 O(1)O(1) 的
需要注意的是,其通过下标访问一个不存在的key时,会报异常,这点可以用后文的defaultdict解决
1 | d = {'str': 1, 'int': 2, 'float': 3} |
set(集合)
set是一个数学意义集合(不可重,无序)的程序实现(内部由哈希表实现)。支持各种集合操作。
1 | s = set() # 创建一个空集合 |
该类型重载了位运算,可以灵活的求集合交,并,补
1 | a = {1, 2, 3} |
其可迭代,故也可以
1 | for i in s: |
这样遍历
类型转换
类型可以函数化,并互相转换
如最常用的 int,str 转换
1 | a = 120 |
还有 list 与 set 互相转换以实现去重操作
Python基础语法
本部分,我将直接列出c++基础语法,并给出在Python上的等价替代。
主函数体
c++main函数基础结构:
1 | int main() { |
Python主函数并不是必要的,完全直接在空文件编写代码,如:
1 | int main() { |
在Python中可以直接写为:
1 | for i in range(10): |
当然,如果实在不习惯,想要和c++风格更加类似,可以按如下写法:
1 | if __name__ == "__main__": |
第一行if __name__ == "__main__":
的意思:字面上,这是一个if判断,而__name__
是一个内置的特殊变量,当我们希望将一个python模块(就是写好的py文件)导入其他python模块时,就只会执行if __name__ == "__main__":
的语句,比如:
1 | print(123) |
print(123)
就不会被执行。
但对于算法竞赛来说,一般不需要多模块操作,该写法只是为了更好的向c++代码风格靠拢。
运算符
python中新添 ** 乘方运算符
无自增 ++ 运算符,无自减运算符 –
条件连接符
python 中均以英文表示条件连接,可读性好些
python | C |
---|---|
and | && |
or | || |
not | ! |
基础语句
循环:
1 | for i in range(n): |
需要注意的是,for i in range(n):
实际上是对range
生成的对象遍历,可以简单理解为对一个[0,1,2…,n−3,n−2,n−1][0,1,2…,n−3,n−2,n−1]的列表遍历,因此我们在循环中修改ii并不会改变之后的循环。比如:
1 | for i in range(n): |
并不会让循环按照0→2→4⋅⋅⋅0→2→4···的顺序进行。
可见,Python中的for循环不如c++中的灵活,因此while的使用频率大大提高了。
关于range函数:
1 | range(start, end, offset) |
三个参数分别代表,起点,终点和步长
range返回的区间是左闭右开的,也就是[start,end)[start,end)
第1和第3个参数可以缺省。
给几个使用实例
1 | for i in range(n): #遍历[0,n) |
分支:
和c++差别不大,给个实例:
1 | if x % 3 == 0: |
可以发现区别只在于Python将else if
合并为了elif
。
但因为引入了in
这个关键字,有了一些更加方便的用法
1 | # a是一个list x是一个int |
函数
Python中函数定义方法很简单:
1 | def func(x): |
Python允许函数定义出现在函数内部
1 | def func1(): |
Output:
1
2
Python允许函数返回多个值
1 | def func(x): |
Output:
11 12
Python中函数内部如果想修改外部数字变量,需要使用nonlocal
或者global
关键字
1 | t = 10 |
如果将global t
注释掉程序会报错。
如果想要使用的变量不是被定义在全局区,而是某个函数体内部则要使用nonlocal
关键字
1 | def work(): |
“头文件”
Python除内置库外,有一些功能需要手动导入模块,有如下几种方法
1 | import math #导入math库 |
第一种方法和后两种调用时有所区别
第一种:
1 | import math #导入math库 |
后两种:
1 | from math import sin, cos#导入math库下的sin,cos函数 |
可见区别就是使用时是否要明确库名,一般在算法竞赛中为了代码简洁,推荐使用后者,但如果要使用from math import *
的方法,将存在一定变量名冲突的风险。
因此,更推荐部分导入。
“宏定义”
Python中没有宏定义,但有替代可以缩短一定码量。如下:
1 | def abcdefgasdas(x): |
我们定义一个及其鬼畜的函数abcdefgasdas(x)
并在之后给其“取别名”为func
,调用func
就等价调用了abcdefgasdas
从而在某些要调用内置函数时,起一个更短的名字,降低码量。
附:之所以可以这样是因为python之中,一切皆对象,故可以一切皆变量(包括函数)
输入输出
输入
Python中的读入和C++还是有很大不同的,需要一定时间适应。
Python读入时都是调用input()
其将返回标准输入中的一行数据(不包括末尾的\n
),其返回的类型统一为字符串,因此还要对其进行变量类型转换。
在算法竞赛中,读入一行数字一般分为可数的几个整数,和一个很长的数组两种形式,我举例说明如何读入:
Input
5
1 3
2 4
2 5
3 2
1 2 3 4 5
1 | # 上面是常用的读入一棵树,并给点赋权值的一种输入格式 |
字符串读入方法就很简单了
1 | s = input() |
读入优化
Python中的真读入优化需要码量巨大,在正式比赛中并不常用,但仍然可以使用如下方法提高一定的读入效率。
1 | import sys |
将其放在Python文件头部即可。可以提高一定效率,但没有c++那般明显。
PS:使用该方法行末的\n
将不会被忽略,在读入字符串数据时尤其要注意
输出
使用print()
进行输出
1 | print(10) # 输出10并换行 |
将输出数据类型转换为字符串,并且将所有中间输出全部加入到此字符串中,最后一次性输出,有时可以提高一定效率,但并不明显。
文件读写
由于前几年蓝桥杯 C++ 组有需要文件读写的情况,所以在此稍微讲解常用用法,具体可见标准库
r 以只读方式打开文件。文件的指针将会放在文件的开头。
1 | file = open(r'input.txt', 'r') # 字符串前的r表示后续紧跟的字符串不进行转义,即原始字符串 |
w+ 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
1 | file = open(r'input.txt', 'w+') |
Python库和函数
介绍一些常用的库和函数,着重和C++的STL对比。
简单函数
1 | # 下列函数可以传入数字或者可迭代数据类型,常传入list |
数学函数
1 | # 内置的,求幂取模用的是快速幂算法,O(log(n)),n为指数 |
math 库中的
1 | factorial() # 阶乘值 |
sort
自定义排序,Python不如C++灵活。首先它只可以对整个序列排序,而无法对部分序列排序,其次自定义方法不如C++的lamda表达式方便。
1 | #自定义排序方法 |
当然,很多时候我们只是想让它根据列表的某一维度排序,这时也可以用python的lambda表达式,代码量少很多。
1 | #a = [[6,5],[3,4],[5,6]] |
sort
是没有返回值的原地排序,如果我们希望获取到这个排序后列表,且不想改变原来的列表时,可以用sorted
函数。自定义
1 | b = sorted(a, key = lambda A: (A[1],-A[0])) |
map (映射)
通过一个函数,对可迭代对象的所有值对象进行修改(创建副本,非原地修改)
1 | a = [1, -2, 3, -4, 5, -6, 7] |
collections
这个库里常用的有deque,defaultdict,Counter
deque
deque对标c++中的双端队列,可以快速在头尾弹出和加入元素。速度比普通队列快,因此在需要队列的场合,统一使用deque
1 | q = deque() # 创建空队列 |
defaultdict
即当键值不存在时,有默认返回值的 dict,其余操作差不多
1 | from collections import defaultdict |
由于defaultdict对于key下标运算返回的是value的引用,若不存在则只能创建一个对象再返回,因此通过下标判断存在与否的方式不推荐,建议使用 in 来判断是否存在
用下标运算来插入与修改
Counter
调用Counter函数计数,返回一个 defaultdict(int)
1 | from collections import Counter |
PS:dict、set和其子类都是用的hash实现,而不是c++中的红黑树,因此没有自动排序功能,目前没有太好的替代。
如果是非标准库的话,有Sorted Container,比较好用。让我们祝福它早日进标准库
heapq(优先队列)
Python中其实有优先队列,但是速度没有heapq快,因此用heapq代替。
heapq提供函数对一个list进行原地的小根堆的维护。
1 | from heapq import heapify, heappop, heappush |
heapq并没有提供方便的重载为大根堆的方法,如果想使用大根堆,一般的技巧是加入值取负值,弹出后再恢复。
基础操作差不多这么多,还有一些其他功能可自行了解。
zip、enumerate函数
这两个函数,都是使得枚举进一步简单化的函数。
zip函数可以同时访问不同list的同偏移量的元素
1 | a = [1,2,3,4,5] |
Output:
1 5
2 4
3 3
4 2
5 1
enumerate则是在访问list中元素时,同时给出元素的下标,下标默认从0开始。
1 | a = [1,2,3,4,5] |
Output:
0 1
1 2
2 3
3 4
4 5
itertools
这个库里的大多函数方法,都是返回一个可迭代对象,因此若要变成list还需list()转换
permutations、combinations
permutations,combinations分别是返回一个可迭代对象(一般是list)的所有排列和组合。使用时需要导入itertools
模块
用法如下:
1 | from itertools import permutations, combinations |
Output:
(1, 2, 3)
(1, 3, 2)
(2, 1, 3)
(2, 3, 1)
(3, 1, 2)
(3, 2, 1)
(1, 2)
(1, 3)
(2, 3)
accumulate(累计)
1 | from itertools import accumulate |
pairwise(成对遍历)
1 | from itertools import pairwise |
functools
reduce (聚合)
其位于functools库里面
对于可迭代对象进行使用,将可迭代对象里的所有值对象,两两聚合,最后返回一个值对象
1 | import operator |
Python小技巧
交换
没有swap函数,但可以这么写,也很方便
1 | a, b = b, a |
列表(其实可以是任何可以迭代对象)解析式
创建列表时,可以用如下方法简化代码
1 | a = [x for x in range(6)] |
列表解析式中中括号中返回的是一个可迭代对象,这个在很多函数中都是可接受的数据类型。结合上面说的“聚合函数”,就可以这样写
1 | # 求列表全体偶数和 |
多维数组
很可惜,python自身没有天然支持固定长度的多维数组(即如C++的int a[5][5][5];
这样的),需要numpy才能很好的使用
但是仍然可以创建,方式如下
1 | a = [[0] * 2 for i in range(4)] |
这是创建了一个 4 * 2 的 二维数组
有人可能疑惑,为什么不能这样创
1 | a = [[0] * 2] * 4 |
这是后果
1 | a = [[0] * 2] * 4 |
第一列全部被修改
最外层的乘4,相当于只是创建了四个引用,引用的都是一个[0] * 2
所以不行
三目表达式
和c++中的三目表达式?:
类似,Python中也有何其类似的语法。
1 | x = 10 |
快速创建一个字典
1 | mp = {} |