サイシードプログラミングコンテスト2021(AtCoder Beginner Contest 219)をPythonで解く

投稿日:  更新日:2022/09/02

Atcoder Python

B!
Daniel AgreloによるPixabay(https://pixabay.com/)からの画像

こんにちは, Shinoryoです.

今回はサイシードプログラミングコンテスト2021(AtCoder Beginner Contest 219)を, Pythonで解いてみた結果を書き連ねていこうと思います.

A - AtCoder Quiz 2

素直に条件式を記述すればよいです.

# 入力
X = int(input())
# 出力
if X < 40:
print(40 - X)
elif X < 70:
print(70 - X)
elif X < 90:
print(90 - X)
else:
print("expert")

B - Maritozzo

文字列S1, S2, S3をリストSに格納します. そして, 文字列Tを1文字目から順に調べていき, 対応する文字列をリストSから探してきて, 答えとなる文字列にくっつけていきます. 文字列をくっつける操作は, 普通の+でOKです.

# 入力
S = []
for _ in range(3):
S.append(input())
T = input()
# 答えとなる文字列を作成
ans = ""
for i in range(len(T)):
ans += S[int(T[i]) - 1]
# 出力
print(ans)

C - Neo-lexicographic Ordering

sortにkeyを渡す方法

リストのsortメソッドやsorted()関数では, 引数keyに関数などを指定することができます. 例えば, key=absと設定すれば, 絶対値にした上でソートされることになります.

今回の場合keyには, ある文字列を引数にとり, 通常の辞書順で対応する文字に置き換える関数を指定します. 例えば, X=bacdefghijklmnopqrstuvwxzyならば, abxbaxに置き換わります.

与えられた新しい辞書順と従来の辞書順の対応付けは, dict型を用いると便利です. キーのリストと値のリストから辞書を作成するには, 複数のリストなどの要素をまとめる関数であるzip()を用いることができます.

import string
# それまでに指定されている辞書dic_ruleに従って、文字列s_parを変換する関数
def strings_change(s_par):
s_change = ""
for s in s_par:
s_change += dic_rule[s]
return s_change
# 入力
X = input()
N = int(input())
S = []
for _ in range(N):
S.append(input())
# 辞書順をdict型に
# 与えられた辞書順を本来の辞書順と対応付ける
dic_keys = [x for x in X]
dic_values = list(string.ascii_lowercase)
dic_rule = dict(zip(dic_keys, dic_values))
# sortメソッドにkeyを指定した上でソート
S.sort(key=strings_change)
# 出力
for i in range(N):
print(S[i])

なんなら自分でソートしてやる方法(1)

やろうと思えば, 自分でソートすることができないわけではありません.

良く知られているソートアルゴリズムの1つに, バブルソート(bubble sort)があります.

リストS=[S1,S2,,SN]が与えられたときに, S1S2を比べてS1<S2となるように, S2S3を比べてS2<S3となるようにしていきます. 最後に, SN1SNを比べてSN1<SNとなるようにすることで, 最終的にSNはその中で最も大きい要素になります.

これを再び繰り返します. S1S2を比べてS1<S2となるように, S2S3を比べてS2<S3となるようにしていきます. 最後に, SN2SN1を比べてSN2<SN1となるようにすることで, 最終的にSN1はその中で最も大きい要素になります.

これを最後まで繰り返せば, ソートが完了するというわけです.

図 バブルソートの概念図. これを繰り返すことによってソートする.

しかしながら, 計算量はO(N2)かかってしまう方法なので, さすがにTLEです.

# 異なる文字列a_par, b_parを比較して、
# a_parの方が辞書順に先ならばTrue、後ならばFalseを返す関数
def left_large_judge(a_par, b_par, dic_rule_par):
# 先頭から文字を比較していって、違う文字が出たらその辞書順を比較する
for i in range(min(len(a_par), len(b_par))):
if a_par[i] != b_par[i]:
return dic_rule_par[a_par[i]] > dic_rule_par[b_par[i]]
# 比較できる最後まで同じなら、文字数で比較する
return len(a_par) > len(b_par)
# 入力
X = input()
N = int(input())
S = []
for _ in range(N):
S.append(input())
# 辞書順をdict型に
dic_keys = [x for x in X]
dic_values = [i for i in range(len(X))]
dic_rule = dict(zip(dic_keys, dic_values))
# バブルソートを実行
for j in range(1, N):
for i in range(N - j):
# 右の方(indexが大きい方)が辞書順で後になるようにしたい
# 左の方(indexが小さい方)が辞書順で後の場合には入れ替えを行う
if left_large_judge(S[i], S[i + 1], dic_rule):
temp3 = S[i + 1]
S[i + 1] = S[i]
S[i] = temp3
# 出力
for i in range(N):
print(S[i])

なんなら自分でソートしてやる方法(2)

比較的計算量が少ないソートアルゴリズムの1つに, マージソート(marge sort)があります.

あらかじめソートしてある2つのリストを合体させて, ソートされた新しいリストを得ることは難しくありません. それぞれのリストの先頭を比較して, 辞書順で小さい方を新リストに加えるという操作を繰り返せばよいだけです. これを利用したのが, マージソートになります.

マージソートは, 分割するフェーズと合体するフェーズの2つに分かれます. 分割するフェーズでは, 多くの場合, ほぼ2分割されるように分解していきます. 最終的にそれぞれ1つの要素しか含まないように分割した後に, それらを順に合体していきます(1つの要素しか含まないリストは, すでにソートされていると見なせます).

図 マージソートの概念図. 分割しまくった後, それらを互いにソートしつつ合体させていく.

分割や合体の回数がO(logN)で, それぞれの分割や合体自体には最大でO(N)かかるので, 必要な計算量はO(NlogN)となります. これならACです.

from collections import deque
# 異なる文字列a_par, b_parを比較して、
# a_parの方が辞書順に先ならばTrue、後ならばFalseを返す関数
def left_small_judge(a_par, b_par, dic_rule_par):
# 先頭から文字を比較していって、違う文字が出たらその辞書順を比較する
for i in range(min(len(a_par), len(b_par))):
if a_par[i] != b_par[i]:
return dic_rule_par[a_par[i]] < dic_rule_par[b_par[i]]
# 比較できる最後まで同じなら、文字数で比較する
return len(a_par) < len(b_par)
# 異なる2つの文字列グループを、辞書順に併合したものを返す関数
# 各文字列グループはすでに辞書順に並んでいる(か、1文字列である)必要がある
def two_str_marge(a_par, b_par, dic_rule_par):
# popleftしたいので、deque型に変換
a_par_deque = deque(a_par)
b_par_deque = deque(b_par)
# 返す文字列グループを格納するリスト
temp4 = deque()
# 文字列グループのどちらかが消えるまで、
# 各文字列グループの先頭の文字列を比較して、temp4に加え続ける
while a_par_deque and b_par_deque:
if left_small_judge(a_par_deque[0], b_par_deque[0], dic_rule_par):
temp4.append(a_par_deque.popleft())
else:
temp4.append(b_par_deque.popleft())
# どちらかの文字列グループが生き残るので、それを最後にtemp4にくっつける
temp4 += a_par_deque + b_par_deque
# temp4を返す
return list(temp4)
# 文字列グループs_parを2つに分割し続ける、
# 1文字列になったらそれらをtwo_str_margeで併合し続ける再帰関数
def marge_sort(s_par, dic_rule_par):
# 1文字以上なら、分割作業
if len(s_par) > 1:
return two_str_marge(marge_sort(s_par[:len(s_par) // 2], dic_rule_par), marge_sort(s_par[len(s_par) // 2:], dic_rule_par), dic_rule_par)
# 1文字ならそのまま文字列を返し、two_str_margeへつなげる
else:
return s_par
# 入力
X = input()
N = int(input())
S = []
for _ in range(N):
S.append(input())
# 辞書順をdict型に
dic_keys = [x for x in X]
dic_values = [i for i in range(len(X))]
dic_rule = dict(zip(dic_keys, dic_values))
# marge_sortでソートする
S_sorted = marge_sort(S, dic_rule)
# 出力
for i in range(N):
print(S_sorted[i])

D - Strange Lunchbox

動的計画法(dynamic programming)を用いて解くことを考えます.

動的計画法を用いて計算する量dp[i][j][k]として, 以下の量を考えます.

  • i種類目までの弁当を購入するか否かを決定したときに, j個のたこ焼きとk個のたい焼きを購入するために買うべき弁当の個数の最小値

0種類目までの弁当を購入するか否かを決定したときに, 0個のたこ焼きと0個のたい焼きを購入するために買うべき弁当の個数の最小値は当然0なので, 初期値としてdp[0][0][0]=0としておきます.

dp[i][j][k]が正しく入力されているとき, i+1番目の弁当を買うか買わないかの選択とすることができます.

  1. 買う場合:新たにA[i]個のたこ焼きとB[i]個のたい焼きを獲得することになります.
    dp[i+1][min(j+A[i],X)][min(k+B[i],Y)]dp[i][j][k]+1で更新します(小さい方の値にする).
  2. 買わない場合:新たにたこ焼きやたい焼きは増えません.
    dp[i+1][j][k]dp[i][j][k]で更新します(小さい方の値にする).

dp[i][j][k]が正しく入力されていない(適切な初期値のままの)場合は, i種類目までの弁当を購入するか否かを決定していくプロセスでは, j個のたこ焼きとk個のたい焼きを獲得することができないということになるので, そのままにしておきます.

最後に出力すべきなのは, dp[N][X][Y]になります. 初期値のままの場合は1を出力することに注意してください.

初期値については, 例えばN+1(N+1個以上の弁当を買うことにはなりません)などが適切でしょう.

# 入力
N = int(input())
X, Y = [int(x) for x in input().split()]
# dp[i][j][k]で「i種類目までの弁当を購入するか否かを決定したときに、
# j個のたこ焼きとk個のたい焼きを購入するために、
# 買うべき弁当の個数の最小値」を計算していく
# 初期値はN + 1を記録しておく
dp = [[[N + 1 for _ in range(Y + 1)] for _ in range(X + 1)] for _ in range(N + 1)]
# 「0種類目までの弁当を購入するか否かを決定したときに、
# 0個のたこ焼きと0個のたい焼きを購入するために、
# 買うべき弁当の個数の最小値」は当然0
dp[0][0][0] = 0
# dp[i][j][k]が正しく入力されているとき、
# i + 1番目の弁当を……
# 1) 買う⇒新たにA[i]個のたこ焼きとB[i]個のたい焼きを獲得する
# dp[i + 1][min(j + A[i], X)][min(k + B[i], Y)]をdp[i][j][k] + 1で更新
# 2) 買わない⇒新たにたこ焼きやたい焼きは増えない
# dp[i + 1][j][k]をdp[i][j][k]で更新
for i in range(N):
A_i, B_i = [int(x) for x in input().split()]
for j in range(X + 1):
for k in range(Y + 1):
# この時点でdp[i][j][k] = N + 1ならば、
# i種類目までの弁当を購入するか否かを決定するプロセスでは
# j個のたこ焼きとk個のたい焼きは購入できない
if dp[i][j][k] < N + 1:
dp[i + 1][min(j + A_i, X)][min(k + B_i, Y)] = min(dp[i + 1][min(j + A_i, X)][min(k + B_i, Y)], dp[i][j][k] + 1)
dp[i + 1][j][k] = min(dp[i + 1][j][k], dp[i][j][k])
# 出力(dp[N][X][Y])
if dp[N][X][Y] < N + 1:
print(dp[N][X][Y])
else:
print(-1)

E以降

E以降は私の能力不足故に省略いたします.

参考にしたサイト等