競技プログラミング コンテストのお供

はじめに

  • コンテスト最中に頭に留めておきたいこと
  • コピペで使えるコード(アルゴリズム、標準入力、イディオム、標準ライブラリ)

をまとめました。コンテストのお供としてお使いいただけたら幸いです。

頭に留めておきたいこと

解法が全く思い浮かばない

  • 問題文をよく見る

    • 問題文の誤読をしていないか確認しましょう。
    • 制約条件をよく読みましょう。
      • あなたが棄却したアルゴリズムで十分間に合う可能性があります。
  • 全探索について考察する

    • 全探索の計算量を見積もりましょう。間に合う可能性があります。
    • 簡単なケースの全探索を考えて、無駄な処理を発見しましょう。どうすれば効率よく探索できますか。
    • 全探索を考えながら、問題を誤読していないか確認しましょう。
  • 何かを固定する

    • 何らかの要素(次元)が数個の場合は固定を考えます。
      • 端を固定する。
      • 三つの場合、中央を固定する。
  • 逆からシミュレーションする

    • 追加ではなく削除、削除ではなく追加。グラフ問題でよくある
  • 書く

    • サンプルケースやそれより小さいケースを自分で考えて、紙とペンを使って書きましょう。

ローカルでエラー

  • エラー文をよく読む
    • 配列外参照をしていないか
    • タイプミスをしていないか
  • テストケースを正しくコピペできているか
  • 実行しているプログラムファイルは正しいか

サンプルは通るけどWA

  • 嘘解法で書いていないか
    • 自信がないのであれば、解法が正しいか検証をしましょう
  • 入力される型の確認をしたか
    • 整数か浮動小数点か
  • 配列のサイズは十分か
    • デバッグ用にと、不十分なサイズで確保していないか
  • 特殊な値での確認はしたか
    • 0, 負の値, 最大の値
  • MODのタイミングは適切か
    • 最後にもMODの計算しているか
  • 精度は十分か
    • 特に小数の出力
  • 再帰関数の呼び出し回数は大丈夫か

その提出、ちょっと待った!

  • printデバッグはきちんと消したか
  • 言語選択を間違えていないか
  • 提出する問題を間違えていないか
  • 提出するコードを間違えていないか
  • 軽微な修正だから・・と思ってテストし直すのをサボっていないか

計算量の目安

1
2
3
10^7 余裕を持って間に合う
10^8 おそらく間に合う
10^9 非常にシンプルな処理でない限り厳しい

計算量オーダーの求め方を総整理! 〜 どこから log が出て来るか 〜より引用させていただきました。

Pythonの場合、この数倍から10倍程度遅いと思っておけば良いでしょう。PyPyの場合、2,3倍程度で済むと思います。

コピペで使えるコード

アルゴリズム一覧(Python)

こちらのページにまとめています

レッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】に沿う形でPythonで問題を解いています。各記事のコードをライブラリのように利用できるはずです。(間違い等あればTwitterまでご連絡ください。)

  • 全探索:全列挙
  • 全探索:工夫して通り数を減らす全列挙
  • 全探索:ビット全探索
  • 全探索:順列全探索
  • 二分探索
  • 深さ優先探索
  • 幅優先探索
  • 動的計画法:ナップザック DP
    • ナップサックDPまたはその亜種
  • 動的計画法:区間 DP
  • 動的計画法:bit DP
  • 動的計画法:その他
  • 最短経路問題:ダイクストラ法
  • 最短経路問題:ワーシャルフロイド法
  • 最小全域木問題
  • 高速な素数判定法
  • 高速なべき乗計算
  • 逆元を使う問題
  • 累積和
    • いもす法
  • Union-Find
  • その他のテクニック
    • 圧縮
    • 単純な幾何計算
  • 実装問題
  • 数学的な問題

標準入力

intを1つ

1
N = int(input())

intを複数

1
a, b = map(int, input().split())

intのlist

1
A = list(map(int, input().split()))

イディオム

二次元リストの初期化(M列N行)

1
visited = [ [0]*M for _ in range(N) ]

二次元リストの転値

1
2
def transpose(A):
return [list(x) for x in zip(*A)]

標準ライブラリ

itertools.combinations

1
2
from itertools import combinations
for m1, m2 in combinations(range(M),2):

itertools.combinations

1
2
from itertools import product
for p in product((0,1),repeat=R): # bit全探索

終わりに

最後までご覧くださり、ありがとうございます!随時更新していきます!

改善点をドシドシお待ちしておりますので、お気軽にTwitterにご連絡ください。

記事情報

  • 投稿日:2020年5月21日
  • 最終更新日:2020年5月21日

DPL2A 巡回セールスマン問題

この記事で使うアルゴリズム

動的計画法
深さ優先探索

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_2_A&lang=ja

この問題は動的計画法で解くことができます。

方針

全探索の計算量はO(N!)となり間に合いません。そこで動的計画法を用います。

経路は全ての頂点を通るため、頂点0からそれ以外の全ての頂点を通ったのち、頂点0に戻るというように考えて問題ありません。

集合Sに訪問済みで頂点vにいるとした時の頂点0まで戻るパスを考え、その経路長をdp[S][v]として計算します。ここでSが集合を表すため、実装上の工夫としてSを長さVのビット列として考えます(1は訪問済み、0は未訪問とします)。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
V, E = map(int, input().split())
INF = 10**10
cost = [[INF]*V for _ in range(V)] # 重み
for e in range(E):
s, t, d = map(int,input().split())
cost[s][t] = d

dp = [[-1] * V for _ in range(1<<V)] # dp[S][v]

def dfs(S, v, dp):
if dp[S][v] != -1: # 訪問済みならメモを返す
return dp[S][v]
if S==(1<<V)-1 and v==0: # 全ての頂点を訪れて頂点0に戻ってきた
return 0 # もう動く必要はない

res = INF
for u in range(V):
if S>>u & 1 == 0: # 未訪問かどうか
res = min(res, dfs(S|1<<u, u, dp)+cost[v][u])
dp[S][v] = res
return res

ans = dfs(0, 0, dp) # 頂点0からスタートする。ただし頂点0は未訪問とする
if ans == INF:
print(-1)
else:
print (ans)

参考

アルゴリズムの多くはプログラミングコンテストチャレンジブック(通称、アリ本)のP173-175を参考にさせていただきました。

記事情報

  • 投稿日:2020年5月21日
  • 最終更新日:2020年5月21日

DPL1D 最長増加部分列

この記事で使うアルゴリズム

動的計画法
二分探索

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DPL_1_D&lang=ja

この問題は動的計画法と二分探索で解くことができます。

方針

dp[n-1]長さnの増加部分列のうち、右端の値として考えられる最小値を格納します。

数列Aの各要素をクエリとした二分探索を行い、dpを更新していけば良いです。

最終的には構成可能なdp[n-1]にはINF以外の値が入るので、それらを数え上げれば答えとなります。

コード

1
2
3
4
5
6
7
8
9
import bisect
INF = 10**10
N = int(input())
dp = [INF] * (N)
for n in range(N):
a = int(input())
i = bisect.bisect_left(dp, a)
dp[i] = a # aがA[:n]中で最大なら、最も左のINFが更新される
print(bisect.bisect_left(dp, INF)) # 最後にINF以外の要素数を数える

記事情報

  • 投稿日:2020年5月21日
  • 最終更新日:2020年5月21日

ALDS1 10C 最長共通部分列

この記事で使うアルゴリズム

動的計画法

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

https://atcoder.jp/contests/abc120/tasks/abc120_d

この問題は動的計画法で解くことができます。

方針

全探索では間に合いませんので、動的計画法により解きます。各クエリに対して計算量はO(MN)となるので、全体の計算量はO(qMN)です。これが想定解法と思われますがPython3の場合はTLEとなってしまいました。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Q = int(input())

def lcs(X, Y):
dp = [[0] * (len(X)+1) for _ in range(len(Y)+1)]
for i in range(1,len(Y)+1):
for j in range(1,len(X)+1):
if X[j-1]==Y[i-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1],dp[i-1][j])
return dp[-1][-1]

ans = []
for q in range(Q):
X = input()
Y = input()
ans.append(lcs(X,Y))
for a in ans:
print(a)

記事情報

  • 投稿日:2020年5月20日
  • 最終更新日:2020年5月20日

ABC120D Decayed Bridges

この記事で使うデータ構造

Union-Find

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

https://atcoder.jp/contests/abc120/tasks/abc120_d

この問題はUnion-Findと呼ばれるデータ構造で解くことができます。

方針

橋を削除していくのではなく、橋が全くない状態から追加していくシミュレーションを行います。

最初の不便さはN*(N-1)/2です。橋を追加するごとに、追加されることで繋がる島の群Aの島の数と群Bのそれの積を引けば良いことになります。このような集合を管理するデータ構造としてUnion-Findを利用すると良いです。

Union-Findとは

互いに素なデータ集合を管理するためのデータ構造です。
Union-Findは以下の二つの機能を持っています。

  • union 二つの集合を併合する
  • find 指定した要素がどの集合に属するかを判定する

データ構造そのものはdisjoint-setと呼び、それに対する操作をUnion-Findと呼ぶ場合もありますが、この記事ではデータ構造をUnion-Findと呼ぶことにします。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class UnionFind:
def __init__(self, n):
self.nodes = n
self.parents = [i for i in range(n)]
self.sizes = [1] * n
self.rank = [0] * n

def find(self, i): # どの集合に属しているか(根ノードの番号)
if self.parents[i] == i:
return i
else:
self.parents[i] = self.find(self.parents[i]) # 経路圧縮
return self.parents[i]

def unite(self, i, j): # 二つの集合を併合
pi = self.find(i)
pj = self.find(j)
if pi != pj:
if self.rank[pi] < self.rank[pj]:
self.sizes[pj] += self.sizes[pi]
self.parents[pi] = pj
else:
self.sizes[pi] += self.sizes[pj]
self.parents[pj] = pi
if self.rank[pi] == self.rank[pj]:
self.rank[pi] += 1
def same(self, i, j): # 同じ集合に属するかを判定
return self.find(i)==self.find(j)

def get_parents(self): # 根ノードの一覧を取得
for n in range(self.nodes): # findで経路圧縮する
self.find(n)
return self.parents

def size(self, i):
p = self.find(i)
return self.sizes[p]


N, M = map(int, input().split())
AB = []
B = []
for m in range(M):
a, b = map(int, input().split())
AB.append((a-1,b-1))

ans = []
score = N * (N-1) // 2
uf = UnionFind(N)
for a, b in AB[::-1]:
ans.append(score)
if not uf.same(a,b):
score -= uf.size(a) * uf.size(b)
uf.unite(a,b)

for score in ans[::-1]:
print(score)

記事情報

  • 投稿日:2020年5月19日
  • 最終更新日:2020年5月19日

JOI2008予選5 おせんべい

この記事で使うアルゴリズム

全探索

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

https://atcoder.jp/contests/joi2008yo/tasks/joi2008yo_e

この問題は全探索を用いて解くことができます。

方針

  • 裏返しによって煎餅は反転するだけですので、ある一連の操作があったとして、その操作の順番を入れ替えても結果は変わりません。
  • ある特定の列や行を二回裏返すと当然元に戻ります。つまり、ある特定の列や行を二回以上裏返す必要はなく、裏返すか裏返さないかの二択を考えれば良いです。

以上の考察により全探索の計算量はO(2^(R+C))となることがわかります。しかし、これだと今回の制約では間に合いません。

そこで、縦R行の固定を考えます。固定のパターンは2^R通りとなります。縦列が固定されると各行を裏返す返さないを独立に決めることができます。具体的には各行について、裏返した場合と裏返さない場合で良い方を選ぶだけです。

この場合、計算量はO(CR2^R)となり間に合います。

コード

このような全探索をbit全探索と呼びます。Pythonではitertools.productを利用すると良いでしょう。なお、PythonではTLEとなってしまうため、PyPyで提出しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from itertools import product
R, C = map(int,input().split())
lines = []
for r in range(R):
line = list(map(int,input().split()))
lines.append(line)
lines = list(zip(*lines))
ans = 0
for p in product((0,1),repeat=R):
sm = 0
for line in lines:
cnt = 0
for r in range(R):
if p[r] == line[r]:
cnt += 1
sm += max(cnt, R-cnt)
ans = max(ans, sm)
print (ans)

記事情報

  • 投稿日:2020年5月19日
  • 最終更新日:2020年5月19日

DSL1A 互いに素な集合

この記事で使うアルゴリズム

Union-Find

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=DSL_1_A&lang=ja

この問題はUnion-Findを用いて解くことができます。

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class UnionFind:
def __init__(self, n):
self.parents = [i for i in range(n)]
self.sizes = [1] * n
self.rank = [0] * n

def find(self, i):
if self.parents[i] == i:
return i
else:
self.parents[i] = self.find(self.parents[i]) # 経路圧縮
return self.parents[i]

def unite(self, i, j):
pi = self.find(i)
pj = self.find(j)
if pi != pj:
if self.rank[pi] < self.rank[pj]:
self.sizes[pj] += self.sizes[pi]
self.parents[pi] = pj
else:
self.sizes[pi] += self.sizes[pj]
self.parents[pj] = pi
if self.rank[pi] == self.rank[pj]:
self.rank[pi] += 1
def same(self, i, j):
return self.find(i)==self.find(j)


n, q = map(int,input().split())
uf = UnionFind(n)
for q in range(q):
cmd, i, j = map(int,input().split())
if cmd == 0:
uf.unite(i,j)
else:
print(int(uf.same(i,j)))

記事情報

  • 投稿日:2020年5月18日
  • 最終更新日:2020年5月18日

ALDS1 11C 幅優先探索

この記事で使うアルゴリズム

幅優先探索

はじめに

カテゴリー競プロ初中級者100問では、Qiitaにて@e869120さんがレッドコーダーが教える、競プロ・AtCoder上達のガイドライン【中級編:目指せ水色コーダー!】としてまとめられている100問をPythonで解いています。

全問題の一覧はこちらです

問題

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_11_C&lang=ja

この問題は幅優先探索を用いて解くことができます。

コード

depthは距離の記録であり、訪問済みかどうかの判定にも用います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import queue
N = int(input())

adj = [[]for i in range(N+1)]
depth = [-1] * (N+1)
for n in range(1,N+1):
V = list(map(int,input().split()))
for v in V[2:]:
adj[n].append(v)

q = queue.Queue()
q.put((1,0)) # 番号、深さ
while(not q.empty()):
x, d = q.get()
if depth[x] != -1:
continue
depth[x] = d
for next in adj[x]:
if depth[next] == -1:
q.put((next,d+1))

for i in range(1,N+1):
print(i,depth[i])

記事情報

  • 投稿日:2020年5月18日
  • 最終更新日:2020年5月18日

ABC168

はじめに

この記事では、AtCoder Beginer Contest 168のA~E問題を解説します。

A - ∴ (Therefore)

https://atcoder.jp/contests/abc168/tasks/abc168_a

inを使うと簡単です。

1
2
3
4
5
6
7
8
N = int(input())
N%=10
if N==3:
print('bon')
elif N in (0,1,6,8):
print('pon')
else:
print('hon')

B - … (Triple Dots)

https://atcoder.jp/contests/abc168/tasks/abc168_b

スライスを利用します。

1
2
3
4
5
6
K = int(input())
S = input()
if len(S)<=K:
print(S)
else:
print(S[:K]+'...')

C - : (Colon)

https://atcoder.jp/contests/abc168/tasks/abc168_c

長針と短針の長さが決まっているので、長針と短針のなす角度を求めることで余弦定理により解を求めることができます。
短針はだけでなくによっても動くので注意します。
また、角度は180度以下になるように変換します。

1
2
3
4
5
6
7
8
9
import math
A,B,H,M = map(int,input().split())
H = (H+M/60)*30 # 360度/12時間 = 30度、長針は短針より60倍遅い
M *= 6 # 360度/60分 = 6度
d = abs(H-M) # 長針と短針のなす角度
if d > 180: # 角度は180度以下
d = 360 - d
C = pow(A**2+B**2-2*A*B*math.cos(math.radians(d)),0.5)
print(C)

D - .. (Double Dots)

https://atcoder.jp/contests/abc168/tasks/abc168_d

幅優先探索によって求めることができます。訪問した際に、前の部屋(親)を記録します。この記録を用いて訪問済みかどうかも判定します。

Pythonで幅優先探索を実装する際は、標準ライブラリのqueueを用いると良いです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import queue
N,M = map(int,input().split())
adj = [[]for i in range(N+1)]
parent = [-1] * (N+1) # -1なら未訪問
for m in range(M):
a,b = map(int,input().split())
adj[a].append(b)
adj[b].append(a)
q = queue.Queue() # 自分の部屋番号、親の部屋番号
q.put((1,0)) # 部屋1の親は部屋0と考える
while(not q.empty()):
x, p = q.get()
if parent[x] != -1: # 訪問済み
continue
parent[x] = p
for next in adj[x]:
if parent[next] == -1: # 未訪問なら
q.put((next,x)) # 探索候補に追加
print ('Yes') # 必ず目標は達成できる
for p in parent[2:]: # 部屋2以降の親(前の部屋)
print(p)

E - ∙ (Bullet)

https://atcoder.jp/contests/abc168/tasks/abc168_e

ポイント

  • aとbの比の情報を考える
    • 約分をしたのちに文字列として格納する
      • 符号がaとbで同じ場合と異なる場合で、別の辞書に格納する
  • どちらか片方のみが0の場合
    • (0,非0)(非0,0)は共存NGで、これらは特別な文字列(例えば0/0)として、別々の辞書に格納すれば問題ない。
  • 両方が0の場合
    • 単独で選ぶパターンのみが有効である。すなわち単純に数え上げて答えに足せば良い。その後イワシは除外する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import math
MOD = 1000000007
N = int(input())

even = dict() # (+,+), (-,-)
odd = dict() # (+,-), (-,+)
ans = 1
zz = 0 # (0,0)の場合は、その数だけ答えをインクリメントすれば良い

for n in range(N):
a,b = map(int,input().split())

if a==0 and b==0:
zz += 1 # かぞえあげる
continue # 考察対象から除外
if a==0:
n_sign = 0
b = 0 # こうしておくと後ほど'0/0'として格納される
elif b==0:
n_sign = 1
a = 0 # こうしておくと後ほど'0/0'として格納される

else:
n_sign = ((a<0) + (b<0)) % 2 # -の出現回数が偶数か奇数か
a = abs(a) # 符号は用済み
b = abs(b) # 符号は用済み
gcd = math.gcd(a,b) # 約分のためにGCDを計算
a//=gcd # 約分
b//=gcd # 約分

if n_sign==1:
s =str(b)+'/'+str(a) # '41/3'のように文字列として扱う
odd[s] = odd.get(s,0) + 1 # かぞえあげる
elif n_sign==0:
s =str(a)+'/'+str(b) # 逆数を考えることで単純な文字列比較に持ち込む
even[s] = even.get(s,0) + 1

for k,v in odd.items():
if k in even: # 共存NGあり
ans *= (2**v + 2**even[k] - 1)
even.pop(k) # 辞書から削除
else: # 共存NGなし
ans *= 2**v
ans %= MOD

for k,v in even.items():
ans *= 2**v # 削除していない分を数える
ans %= MOD

ans -= 1 # 1匹も選択しないのはNG
ans += zz
ans %= MOD
print (ans)

関連記事

過去のABC解説記事です。

  • ABC167
    • A-E問題を解いています。
  • ABC166
    • A-F問題を解いています。
  • ABC165
    • A-F問題を解いています。
  • ABC164
    • A-E問題を解いています。
  • ABC163
    • A-D問題を解いています。
  • ABC162
    • A-E問題を解いています。

記事情報

  • 投稿日:2020年5月18日
  • 最終更新日:2020年5月18日