目次

Python 競プロ用 Tips

既に巷に溢れている気もするが、まぁリンク集+自分用メモ的な感じで。

Pythonで競プロに参加する上での、言語依存な書き方など。散発的になるのは仕方なし。

環境

AtCoderにおけるPythonの環境

OS
import subprocess
subprocess.run(['cat', '/etc/os-release'])

↓
...
VERSION="18.04.4 LTS (Bionic Beaver)"
...
モジュール一覧
import sys
import subprocess
subprocess.run([sys.executable, '-m', 'pip', 'list'])

↓

asn1crypto (0.24.0)
cryptography (2.1.4)
Cython (0.29.16)
decorator (4.4.2)
idna (2.6)
joblib (0.14.1)
keyring (10.6.0)
keyrings.alt (3.0)
llvmlite (0.31.0)
netifaces (0.10.4)
networkx (2.4)
numba (0.48.0)
numpy (1.18.2)
pip (9.0.1)
pycrypto (2.6.1)
Pygments (2.2.0)
pygobject (3.26.1)
python-apt (1.6.5+ubuntu0.2)
pyxdg (0.25)
PyYAML (3.12)
scikit-learn (0.22.2.post1)
scipy (1.4.1)
SecretStorage (2.3.1)
setuptools (46.1.3)
six (1.11.0)
unattended-upgrades (0.1)
wheel (0.30.0)

入出力

主に4つの方式がある

input

基本。1行読み込んだ文字列が得られる。末尾の改行は取り除かれる。

s = input()
print(s)
print(type(s))

# 入力
# 3 1 4 1 5

# => '3 1 4 1 5' <class 'str'> 型

sys.stdin 系

入力が大量で、速さを求めるとき。

注意点

import sys

s = sys.stdin.readline()
print(s)
print(type(s))

t = sys.stdin.readlines()
print(t)
print(type(t))

# 入力
# 3 1 4 1 5
# 9 2 6 5 3
# 5 8 9 7 9

# s: '3 1 4 1 5\n'                   <class 'str'>
# t: ['9 2 6 5 3\n', '5 8 9 7 9\n']  <class 'list'>

入力が全て整数の時の高速化

なるべく read() で一括で読み、一括で split() や map(int, ○○) に渡した方が速い。

N M
A1 A2 A3 ... AN
B1 B2 B3 ... BM

のようなデータなら、アスタリスク記法を使って以下のようにすると、簡潔にA,Bを整数リストで得られる。

n, m, *ab = map(int, sys.stdin.read())
aaa = ab[:n]
bbb = ab[n:]

# 入力
# 5 4
# 1 2 3 4 5
# 6 7 8 9

# ab:  [1, 2, 3, 4, 5, 6, 7, 8, 9]
# aaa: [1, 2, 3, 4, 5]
# bbb: [6, 7, 8, 9]

少し困るのが、「$N$ 行に渡って $a_i,b_i,c_i$ 3種類の整数が与えられる」みたいな時、一括で読んでしまうと1行ずつの情報としてまとめにくい。

N
a1 b1 c1
a2 b2 c2
...
aN bN cN

以下のようにすると、簡潔に [(a1,b1,c1), (a2,b2,c2), …] にできる(ちょっとテクニカル)。1行ずつ読んでリストに追加していくより1.5倍程度速い。

n = int(sys.stdin.readline())
mp = map(int, sys.stdin.read().split())
abc = list(zip(mp, mp, mp))  # ←1行に含まれるデータ数だけmpを連ねる

# 入力
# 3
# 1 2 3
# 4 5 6
# 7 8 9

# abc: [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

実験コード

sys.stdin.buffer 系

より大量の入力を高速にさばきたい場合。ただし癖が強いので十分な理解が必要。

それぞれの役割は、sys.stdin.○○ と同じ。末尾に改行が残るのも同じ。

ただし、以下に示す注意点がある。

他の入力方式との混用を避ける

buffer.read系を用いる場合は、事前に input や sys.stdin.read系を使っていてはいけない。

input等を使って読み込んだ時点で、既にある程度先までバッファで読まれているため、あらためて buffer.read系で読んだとき、中途半端な位置からの読み込みとなってしまう。

buffer.read系を用いる場合は、最初からそれのみで完結させる。

入力の型はbytes型

まず、Pythonの文字列(っぽいもの)を表す型として、str型の他に、bytes型(バイナリシーケンス型)がある。

str型はPython3内部での文字を扱う形式であるUnicode型に変換された上での文字列情報だが、bytes型は、文字コード不定の、まさに入力バイナリ列そのものである。

このbytes型は、

なので、実質困ることがあるというと、入力に文字列が含まれるときである。 sys.stdin でも十分高速なので、文字列が含まれる場合は無理せずそちらを使った方が面倒がない。

どうしてもsys.stdin.bufferを使いたい場合、(競プロでは普通、英数字しかないので)decode('ascii') 等を指定してデコードすれば、str型となる。

collections

deque

numpy

scipy

sparse graph(経路探索)

実際は、探索しながら他の値も計算しなければならない、とかで、経路探索であってもこれをそのまま使えない問題も多い。

典型的な問題では使えばよいが、一応自分でアルゴリズムを書けることも重要。

Convex Hull(凸包)

たまに出る幾何で、与えられた2次元座標の点を全て包む最小の多角形を求める問題。

経路探索と同様、なかなかそのまま使える機会は少ないが、一応できるということを知っておくだけでも。

Numba

多くの競プロサイトはNumbaは使えないことが多いが、AtCoderは2020/04より使えるようになった。

そもそも、Pythonはお手軽に書けるのが一つの利点であり、 使える関数やデータ型が限定されるNumbaに書き換えてまで Pythonにこだわる意味があるのか、というのはある。
AtCoder以外では今のところ使えないし、複雑な処理ではコンパイル可能なコードにするのが難しいこともある。

そうは言っても、メイン言語がPythonの人にとって、1からC++などで書きなおす以外にPythonで通せる幅が広がったのは歓迎すべきことだ。

Numbaの中でも使い方は複数あるが、競プロの文脈でポイントとなるのは、以下だろう。

このことを念頭に置いて、スニペットを作成する。

スニペット

以下、考慮すべきことと採択理由をメモしている。

事前コンパイル

Numbaのコンパイルには、大別して実行時コンパイル(JIT)と事前コンパイル(AOT)がある。
JITはその名の通り実行時間にコンパイル時間が含まれてしまうので、基本的にはAOTを行いたい。

ただし、JITでも cache=True(キャッシュを有効にする)オプションを指定でき、 それを用いると自動的にコンパイル結果をファイルとして保存し、以前のコンパイル結果があればそれを利用することができる。実質AOT。 1)

AtCoderのジャッジシステムでは、テストケース実行前に提出コードをコンパイルするフェーズがある。 スクリプト言語にとっても起動を速くする効果があったりするため、行われている。

Pythonでは ONLINE_JUDGE を実行時オプションとした処理が1回走る。 Numbaでのコンパイルも、このコンパイルフェーズを利用して行う。

つまり、以下のいずれかを行えばよい。

一長一短あるので使い分け。でも基本的にはJITの方が使い勝手がよい。

メリット・デメリット

コードをスッキリと保ちたいならJIT、ギリギリまで速度を求めたければAOT、でよいと思う。

こう書くとAOTはやたら面倒そうだが、スニペットとしてまとめ、使い方さえ把握しておけば大丈夫。

ローカル環境でのAOT

AOTの方を採用する場合でも、なるべく提出コードを変えずにローカルで動くようにしたい。

AOTを用いる場合、ジャッジシステムでの挙動と同様にコンパイルと本処理を切り分けると、2回実行する必要があり、煩わしい。 また、1つのコンパイル結果をそんなに何回も使い回さない。

それなら、毎回数秒かかってしまうが、常にコンパイル→本処理と1回で流すようにする方が楽。
さらに、それならローカルではAOTでなくJITでいいよね、という話になる。

実際、AOTをローカル環境でやろうとすると、C++コンパイラが必要、JITよりも若干コンパイル時間がかかる、などあるので、手元ではJITの方で十分と感じる。

OS環境はWindowsなら os.name == 'nt'、Mac/Linuxなら os.name == 'posix' で、ローカルがWindowsであればジャッジシステムと区別できる。 これで切り分けて、ローカルならJITコンパイルして実行するようにスニペットを作っておく。

入力受け取りと型指定

Numba関数内ではinput()など入力受け取り関数は使えない。素のPythonで受け取って、引数として与える必要がある。

そして、関数をコンパイルするには、引数と戻り値の型を指定してやる必要がある。

厳密には、JITの場合は型指定せずとも動くのは動く。 ただしその場合、実際に関数が呼ばれたタイミングで自動推論→コンパイルされるのであり、逆に言えば呼ばれるまでされない(本来の意味での実行時コンパイルになる)。 型指定すれば定義された時点でコンパイルされるので、コンパイルフェーズに行いたければやはり型を指定する必要がある。

入力の共通化

毎回きちんと型指定するのは手間だし、どう書けばいいんだっけ、と間違いも起こりやすい。

AtCoderでは、文字列を含まない問題の入力は全て整数であることが多い。 そのため、以下のようにすると、一括で全入力を1つのNumpy配列にできる。

inp = np.fromstring(sys.stdin.readline(), dtype=np.int64, sep=' ')

これを唯一の引数とするようにNumba関数を作ると、型指定は常に '(i8[:],)' で済む。戻り値は省略しても自動推論される。

問題に合わせて変更する箇所が少なくなり、「Numba関数の中身だけ書けばいい」という状態にできる。

小数や文字列を含む場合などは、共通化しにくいのでその都度、型を適切に合わせることにする。

実装例

以下を参照させていただいた。あまり変わっていないが、ローカル環境でも同一コードで動くようにした。

ローカル環境がWindowsであることを前提としており、os.name でLinux(AtCoderのジャッジシステム)と区別できる。 MacやLinuxなら、platform モジュールなどでジャッジシステムと違う箇所を探すか、ローカル環境のみ実行時オプションを付けて識別するなどする。

基本的に solve() の中身と、あと必要なら最後の出力整形部分のみ書けばよい。

実装例

関数を分ける

処理の切り出しなどで関数を複数に分けたくなったとき、以下の2つがあるが、

前者は関数毎にコンパイル指定(JITなら@njitデコレータの付与、AOTならcc.exportへの登録)の必要がある一方、 後者は大枠関数のみの指定で特に問題なく内部の関数も型推論してくれる。

ただし、内部関数が再帰を含む場合、コンパイルが通らない。

numba.core.errors.NotDefinedError: Variable '(内部関数名)' is not defined.

通常は1つの大枠関数に入れた方が手間が少ないのでそうし、再帰関数のみ個別にコンパイルする。

独自クラス

@jitclass デコレータによりJITでのコンパイルは可能だが、これはこれで関数とは別の書き方が必要であり、覚えることとが増えてしまう。

AOTでのコンパイル方法は探したけど見つかってない。

クラスは、1つのオブジェクトに関係する処理をまとめることで理解しやすくしてくれる点はあるが、競プロのような短いコードでは必須でもないので、今のところはクラスを使わない書き方で対処する方針で。

エラー対策

Numbaを使うと、エラーのデバッグに若干手間取ることがある。

特に、ローカルでは動くのにサーバではREが出た時に厄介。

Numbaはバージョンの違いで使える機能に結構差が出てくる。
AtCoderの現在のNumbaのバージョンは 0.48.0 なので、差し支えなければこれに揃える。
ただ、Numbaだけを揃えても、他のNumPy等のモジュールの違いのせいなのか不明だが、REが出ることがある。

そのため、テストケースが全てREの場合はコンパイルフェーズの可能性も視野に入れる。

また、

このため、

という問題により、ちょっとデバッグがしづらい。

これの解決策は今のところ見つけられていない。Numbaを選択するときのリスクの1つ。

コンパイル時間超過

Numbaの中身が複雑だったりしてコンパイルに時間がかかりすぎると、どうも勝手に打ち切られるらしい。

その場合、コンパイルフェイズでモジュールが生成されない→実行フェイズでモジュールが存在しないので、全てのテストケースで RE となる。

コンパイル時間を短くするような書き方については要調査。

1)
キャッシュファイルは、スクリプトと同階層の __pycache__ フォルダに生成される