個人的に競プロでよく使うんだけど、毎回調べたりしちゃうポイントとかをまとめてます。
基本的に、AtcoderでPythonを用いる場合は、言語はPyPy3を用いた方が高速ですが、再帰を用いる場合など問題によってはPython(CPython)でしか実行時間内に収まらない場合があります。(ここがPythonのめんどくさいところ。)
コンテンツ一覧
入力の高速化
Pythonの標準の 入力メソッドである、input()ですが、大量の行を読み込む際は遅い。普通の問題であればさほど問題にはならないが。。
通常の使い方
A = int(input())
N,M = map(int,input().split())
S = list(map(int,input().split()))
for _ in range(N):
L.appnend(input())
from sys import stdin
N,M = map(int,stdin.readline().split())
S,T = [],[]
for n in range(N):
S.append(stdin.readline())
for m in range(M):
T.append(stdin.readline())
print(N,M)
print(S)
'''
4 4
['ab\n', 'cd\n', 'ef\n', 'gh\n']
改行が残る
'''
T.append(stdin.readline()[:-1])
['ab', 'cd', 'ef', 'gh']
リストを空白区切りで出力する
例えば、[1,2,3,4,5]を、1 2 3 4 5 と出力する必要性がある場面。
l = [1,2,3,4,5]
print(*l)
'''出力
1 2 3 4 5
'''
リストを文字列に直す
リストを文字列して繋げたい場面がたまにあります。
C = [0,1,1,1,0,0]
column = "".join(map(str,C))
'''出力
011100
'''
ちなみに、PyPyは文字列の足し算がめちゃくちゃ遅いという性質があり、注意が必要です。以下のように、リストを連結させて文字列を生成する場合、Python3.8.2では61msで実行できるのに対して、PyPyは3638msかかりました。
A = list(range(100000))
c = ""
for a in A:
c += str(a)
print(c)
しかし、PyPyでも上記のようにjoinを使えば、80msほどまで短縮できます。
A = list(range(100000))
c = "".join(map(str,A))
print(c)
setとlist
set型は集合型のデータ構造で、list型に似た配列構造を持ちますが、重複を許容しないユニークな要素の集合です。list型は[]で表現されますが、set型は辞書(dict)と同じ {} で表現されます。
a = set([1,3,53,2,3,2])
print(a)
#{1, 2, 3, 53}
リストをset()関数に入力すると、重複が削除されるため、リストのユニークな要素を知りたいときなどにも使います。
生成、データの追加、削除
空のset型を生成するときは、set()関数を使います。list型のように、{}で生成すると辞書型のデータが生成されてしまいます。
list型は、pop(インデックス)で指定したインデックスの要素を削除できます。pop(0)でリストの先頭を削除することも可能ですが、これはリスト長分のO(N)の計算量が発生します。(末尾の削除はO(1)です)先頭要素の削除が必要な場合は、キューを用いましょう。途中要素も当然後続のリスト長分の計算量が発生しますので、llist型で削除をする場合は、基本的に最後尾の削除しか許容されないと考えたほうが良いと思います。
set型では、基本的にremoveか、discardを用いて要素削除を行います。両者とも指定した要素を削除してくれますが、discardは集合内に指定した要素がなくてもエラーを吐かないという違いがあります。
#空のset型、list型の生成
list_example = list() #またはlist_example = []
set_example = set() #set_example = {}とすると辞書型で定義されてしまう。
#データの追加
list_example.append(5)
set_example.add(5)
#list型の要素削除
list_example.remove(5) #指定した要素に一致するものを削除。複数ある場合は、インデックスが小さい方が削除される。
list_example.remove(6) #指定した要素がリスト内に存在しない場合はエラー
a = list_example.pop() #末尾の要素を削除し、returnする。インデックスをしていすることも可能。削除する要素がないとエラー
#set型の要素の削除
set_example.remove(5) #指定した要素を削除する。指定する要素が集合にないとエラー
set_example.discard(5) #指定した要素を削除する。指定する要素が集合になくてもエラーは吐かない
a = set_exmaple.pop() #集合の中からランダムな要素を削除する。削除したものをreturnします。
強制終了
exit()でプログラムを中断できます。例えば何か、条件に合致したものが見つかった際にそこでプログラムを終了させたい時とかに使えます。
if S[0] == 0:
for sl in split_list:
if sl in column:
print("Yes")
exit()
print("No")
辞書の使い方
複雑な条件のif文、例えば入力された文字列ごとに出力を変えるものなどには、辞書型が有効です。
D = {"Monday":5,
"Tuesday":4,
"Wednesday":3,
"Thursday":2,
"Friday":1}
切り上げ
\(\ 1.6 \) を \(\ 2 \) として扱いたい場合があります。ただ単純にint(1.6) としたり切り捨除算演算子(//)を用いると、\(\ 1 \) として出力されてしまいます。mathライブラリの「math.ceil()」で切り上げられます。出力は自動で、int型になります。
ただし、マイナスの時は注意が必要です。math.ceilでは \(\ -1.6 \) は \(\ -1 \) と出力されてしまいます。
import math
math.ceil(5/3)
'''出力
2
'''
math.ceil(-5/3)
'''出力
-1
'''
math.ceilを用いいない場合は以下のように書けます。
(x + y - 1) / y
再帰関連
この問題を例に取り上げます。
再帰上限の解放
AtCoderでは PyPy、Python共に再帰呼び出しの上限が1000回に設定されており、それを超える実行は「実行時エラー」となります。
1000回の上限を超える実行は普通に考えられるので、この上限の設定値を上げておく必要があります。上記の問題も1000回では、上限に達してしまうので、制約等に合わせて、上限値を設定しておきます。
import sys
sys.setrecursionlimit(2000000)
再帰はPyPyは遅い
上記の問題、以下でACとなりますが、実行時間内に収まるのはPythonで実行した場合のみで、PyPyだとTLEとなります。PyPyはループ処理(for文とか)ではPythonよりも高速になることが多いですが、再帰処理となるとPython の方が高速です。
import sys
sys.setrecursionlimit(2000000)
N,M = map(int,sys.stdin.readline().split())
graph = {i+1 : [] for i in range(N)}
for _ in range(M):
u,v = map(int,sys.stdin.readline().split())
graph[u].append(v)
graph[v].append(u)
result = 0
def dfs(v,visited):
global result
if result == 1000000:
return
result += 1
visited[v-1] = 1
for neibor in graph[v]:
if not visited[neibor-1]:
dfs(neibor,visited)
visited[v-1] = 0
visited = [0] * N
dfs(1,visited)
print(result)
深さ優先探索・幅優先探索
グラフ問題での探索などによく使います。深さ優先探索は行き止まりまでとりあえず行き続けることを繰り返す探索方法で、幅優先探索はスタート地点から近いノードから順番に見ていく探索方法です。
深さ優先探索
深さ優先探索は、主に再帰を使う実装とスタックを使う実装があります。再帰を使う方が実装はシンプルになり、再帰を使った実装の方が一般的な印象がありますが、実行順序が直感的にはややわかりづらくなる印象があります。それに加え、Pythonは前述のように再帰回数に上限が設定されているため、そこにも注意が必要です。
N,M = map(int,input().split())
V = {i+1:set() for i in range(N)}
for _ in range(M):
u,v = map(int,input().split())
V[u].add(v)
V[v].add(u)
#再帰で実装した例
def dfs(v,visited,comp):
visited.add(v)
comp.add(v)
for neibor in V[v]:
if neibor not in visited:
dfs(neibor,visited,comp)
#スタックを用いて実装した例
def dfs_stack(v,visited,comp):
stack = [v]
while stack:
#一番最後に追加されたものを引っ張り出す。
vertex = stack.pop()
comp.add(vertex)
#訪れたことのない頂点ならば処理を実行
if vertex not in visited:
visited.add(vertex)
#接続する頂点が訪れたことのないものならスタックに追加
for neibor in V[vertex]:
if neibor not in visited:
stack.append(neibor)
visited = set() #set型で訪れたことのあるノード一覧を定義。
comps = [] #グラフの連結成分を格納するリスト
for v in V:
comp = set()
if v not in visited:
dfs_stack(v,visited,comp)
comps.append(comp)
print(len(comps))
幅優探索
幅優先探索はキューを用いて実装します。
from collections import deque
def bfs(v,visited,comp):
queue = deque([v])
comp.add(v)
visited.add(v)
while queue:
vertex = queue.popleft()
for neibor in V[vertex]:
if neibor not in visited:
visited.add(neibor)
queue.append(neibor)
comp.add(neibor)
二分探索・二分法(bisect)
二分探索
二分探索はソート済みリストから、対象が存在するかを探索する際などに用いる。
def b_s(data,value,right): #binary_search
left = 0
right = right
while left <= right:
mid = (left + right) // 2
if data[mid] == value:return True
elif data[mid] < value:left = mid + 1
else:right = mid - 1
return False
二分法(bisect())
itertools(組み合わせイテレータ)
itertoolsとは、Python標準のイテレータ生成ツールです。イテレータとは、データの流れを覚えているものです。使い心地としてはリストと似てるとこがあるので、若干混同しがちです。簡単にいうなら、リストはメモリを使って要素を全部丸覚えしているデータ構造で、それに対して、イテレータは要素の法則性を覚えているデータ構造です。
例えば、\(\ [ 1, 3, 5 ,7, ...] \) というような初項 \(\ 1 \) 、交差\(\ 2 \) の数列に対して、リストは項数分のメモリを使って覚える必要がありますが、イテレータは「初項 \(\ 1 \) 、交差\(\ 2 \) の数列」という覚え方をします。故にイテレータの方がメモリ効率がよくなります。
等差数列くらいであれば、イテレータを使う必要まではないかもしれません。しかし、itertoolsの中の「組み合わせイテレータ」というものが大変便利で、これだけで他の言語ではかなりの行数記述する必要がある問題が簡単に解ける場合があります。
product()
集合の直積集合と呼ばれるもので、与えられた集合のあり得る全ての並びの組み合わせを出力します。例えば、\(\ [ 1,2,3,4,5] \) を何度でも使ってあり得る全ての\(\ 3\) 桁の並べ方を求める際に有効です。\(\ ( 1,2,3) \) と\(\ (2,1,3) \) は別物扱いで、\(\ (5,5,5) \)のような重複も許容されます。なので、この例では単純に\(\ 5^3 =125 \) 通りの組み合わせが得られます。
import itertools
for i in itertools.product(range(1,6),repeat=3):
print(i)
''' 出力
(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 1, 4)
(1, 1, 5)
(1, 2, 1)
(1, 2, 2)
(1, 2, 3)
.
.
.
(5, 5, 3)
(5, 5, 4)
(5, 5, 5)
'''
これよりも、次のpermutationsとcombinationsが活躍することが多いです。
permutations
重複なしのあらゆる並びを得られます。例えば、\(\ [ 1,2,3,4,5] \) を一回ずつだけ使って得られる\(\ 3\) 桁の並べ方を求める際に有効です。\(\ ( 1,2,3) \) と\(\ (2,1,3) \) は別物扱いで、\(\ (5,5,5) \)や\(\ (5,4,5) \) のような重複を含みものは許容されません。なので、組み合わせとしては \(\ 5 \times 4 \times 3 =60 \) 通りとなります。
import itertools
for i in itertools.permutations(range(1,6),3):
print(i)
'''
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 2)
(1, 3, 4)
(1, 3, 5)
(1, 4, 2)
.
.
.
(5, 4, 1)
(5, 4, 2)
(5, 4, 3)
'''
combinations()
並べ方ではなく、単純に組み合わせを吐き出してくれます。組み合わせは辞書順に吐き出してくれます。例えば、\(\ [ 1,2,3,4,5] \) の中から\(\ 3\)つ重複なしで選択する場合に有効です。\(\ _5C_3 = 10\) 通りの組み合わせが得られます。
for i in itertools.combinations(range(1,6),3):
print(i)
'''出力
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 4)
(1, 3, 5)
(1, 4, 5)
(2, 3, 4)
(2, 3, 5)
(2, 4, 5)
(3, 4, 5)
'''
combinations_with_replacement()
上記 combinations に重複を許容したものです。各要素を何回でも選べる際の組み合わせということです。
例えば、\(\ [ 1,2,3,4,5] \) の中から使って得られる\(\ 3\) 桁の並べ方を求める際に有効です。(重複して選んでも良く、選ばないものがあっても良い)\(\ 5 \) 種類のものから\(\ 3 \) つ選ぶので \(\ _5H_3 = \frac{((5-1)+3)!}{(5-1)! \times 3!} =35\)通りの組み合わせが得られます。
for i in itertools.combinations_with_replacement(range(1,6),3):
print(i)
''' 出力
(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 1, 4)
(1, 1, 5)
(1, 2, 2)
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 3)
(1, 3, 4)
(1, 3, 5)
(1, 4, 4)
(1, 4, 5)
(1, 5, 5)
(2, 2, 2)
(2, 2, 3)
(2, 2, 4)
(2, 2, 5)
(2, 3, 3)
(2, 3, 4)
(2, 3, 5)
(2, 4, 4)
(2, 4, 5)
(2, 5, 5)
(3, 3, 3)
(3, 3, 4)
(3, 3, 5)
(3, 4, 4)
(3, 4, 5)
(3, 5, 5)
(4, 4, 4)
(4, 4, 5)
(4, 5, 5)
(5, 5, 5)
'''