LoginSignup
12
6

More than 1 year has passed since last update.

【AtCoder解説】PythonでABC222のA,B,C,D問題を制する!

Posted at

ABC222A,B,C,D問題を、Python3でなるべく丁寧に解説していきます。

ただ解けるだけの方法ではなく、次の3つのポイントを満たす解法を解説することを目指しています。

  • シンプル:余計なことを考えずに済む
  • 実装が楽:ミスやバグが減ってうれしい
  • 時間がかからない:パフォが上がって、後の問題に残せる時間が増える

ご質問・ご指摘はコメントツイッターマシュマロまでどうぞ!

Twitter: u2dayo
マシュマロ: https://marshmallow-qa.com/u2dayo
ほしいものリスト: https://www.amazon.jp/hz/wishlist/ls/2T9IQ8IK9ID19?ref_=wl_share

よかったらLGTM拡散していただけると喜びます!

目次

ABC222 まとめ
A問題『Four Digits』
B問題『Failing Grade』
C問題『Swiss-System Tournament』
D問題『Between Two Arrays』

アプリ AtCoderFacts を開発しています

コンテストの統計データを見られるアプリ『AtCoderFacts』を作りました。
現在のところ、次の3つのデータを見ることができます。

  • レート別問題正解率
  • パフォーマンス目安
  • 早解きで上昇するパフォーマンス

今後も機能を追加していく予定です。使ってくれると喜びます。

ABC222 まとめ

全提出人数: 7180人

パフォーマンス

パフォ AC 点数 時間 順位(Rated内)
200 AB------ 300 9分 5404(5187)位
400 AB------ 300 3分 4466(4249)位
600 ABC----- 600 65分 3699(3484)位
800 ABC----- 600 37分 2918(2703)位
1000 ABCD---- 1000 87分 2198(1985)位
1200 ABCD---- 1000 54分 1601(1390)位
1400 ABCD---- 1000 30分 1136(931)位
1600 ABCDE--- 1500 83分 779(590)位
1800 ABCDE--- 1500 54分 518(347)位
2000 ABCDEF-- 2000 93分 319(188)位
2200 ABCDEF-- 2000 61分 196(94)位
2400 ABCDE-G- 2100 64分 111(44)位

色別の正解率

人数 A B C D E F G H
3052 98.8 % 97.2 % 30.5 % 5.1 % 0.6 % 0.2 % 0.2 % 0.0 %
1322 99.4 % 99.5 % 75.1 % 27.2 % 1.6 % 0.6 % 0.8 % 0.0 %
1024 99.8 % 99.6 % 94.1 % 75.4 % 10.7 % 2.6 % 1.7 % 0.0 %
651 99.4 % 99.4 % 98.2 % 94.6 % 51.0 % 12.1 % 4.3 % 0.0 %
366 99.2 % 99.2 % 98.9 % 98.6 % 84.4 % 40.7 % 11.5 % 0.0 %
164 93.9 % 93.3 % 91.5 % 93.3 % 84.8 % 54.3 % 26.2 % 0.0 %
32 96.9 % 96.9 % 96.9 % 96.9 % 93.8 % 81.2 % 62.5 % 0.0 %
27 100.0 % 100.0 % 100.0 % 100.0 % 92.6 % 88.9 % 96.3 % 18.5 %

表示レート、灰に初参加者は含めず

A問題『Four Digits』

問題ページA - Four Digits
コーダー正解率:98.8 %
コーダー正解率:99.4 %
コーダー正解率:99.8 %

入力

$N$ : 整数

実装

色々な実装がありますが、$N$ を文字列で受け取り、N.zfill(4)で $4$ 桁のゼロ埋めされた文字列を出力するのが楽です。

コード

N = input()
print(N.zfill(4))

B問題『Failing Grade』

問題ページB - Failing Grade
コーダー正解率:97.2 %
コーダー正解率:99.5 %
コーダー正解率:99.6 %

入力

*$N$ : 人数
$P$ : 合格点
$a_i$ : 学生 $i$ の点数 *

考察

$a_i < P$ の要素がいくつあるか数える問題です。

実装

forループでひとつずつ確認すれば良いです。

コード

N, P = map(int, input().split())
A = list(map(int, input().split()))
ans = 0

for x in A:
    if x < P:
        ans += 1
print(ans)

sum関数と内包表記を使う方法(おまけ)

わかるならこのように書いたほうが短くて楽かもしれません。

N, P = map(int, input().split())
A = list(map(int, input().split()))
print(sum(x < P for x in A))  # x < P なら Trueになります。Trueは1, Falseは0と同じものとして扱われるので、sum関数で足し合わせればいいです

C問題『Swiss-System Tournament』

問題ページC - Swiss-System Tournament
コーダー正解率:30.5 %
コーダー正解率:75.1 %
コーダー正解率:94.1 %

入力

$N$ : 参加人数
$M$ : じゃんけん大会のラウンド数
$A_{i,j}$ : 選手番号 $i$ の人が、ラウンド $j$ で出す手(GCPのどれか)

考察

制約が小さいので、シミュレーションすることを考えます。

インデックスに合わせるため、一番上の順位を $1$ 位ではなく $0$ 位とします。ラウンドごとに、$0\le{i}\lt{N}$ について、$2i$ 位の人と $2i + 1$ 位の人をじゃんけんさせて、勝利数を更新します。

ラウンドが終了するごとに、勝利数が多い順、勝利数が同じなら選手の番号が小さい順にソートして、各プレイヤーの順位を求めます。

$M$ ラウンド終了したら、順位が上の順に選手番号を出力します。

実装

rankという配列を作ります。各要素は、$(勝利数, 選手の番号)$ のリストです。手前から2人ずつ取り出して、じゃんけんで勝ったほうの勝利数を増やします。あいこならどちらの勝利数も変更しません。

ラウンドがすべて終わったら、配列をソートして順位ごとに並び替えます。

ここで、勝った人に $+1$ ポイントするかわりに、 $-1$ ポイントすることにすると、普通にソートするだけで順位通りに並び替えてくれます。$+1$ ポイントにすると、選手の番号が小さい順にソートして、その後勝利数が大きい順にソートする必要があります。

コード

N, M = map(int, input().split())
hands = []
for _ in range(2 * N):
    s = input()
    hands.append(s)

rank = [[0, i] for i in range(2 * N)]

# D[h1][h2]: じゃんけんでh1, h2を出して、h1が勝つなら1, h2が勝つなら2
D = {"G": {"C": 1, "P": 2},
     "C": {"P": 1, "G": 2},
     "P": {"G": 1, "C": 2}}

for j in range(M):
    for i in range(N):
        p1 = rank[2 * i][1]  # 2*i位の選手番号
        p2 = rank[2 * i + 1][1] # 2*i+1位の選手番号
        h1 = hands[p1][j]  # p1がjラウンド目に出す手
        h2 = hands[p2][j]  # p2がjラウンド目に出す手
        if h1 == h2:  # あいこです
            continue
        else:
            winner = D[h1][h2]
            if winner == 1:
                rank[2 * i][0] -= 1  # ここで勝ったほうの勝利カウントを-1すると、ソートが楽です
            else:
                rank[2 * i + 1][0] -= 1
    rank.sort()  # 勝利カウントが小さい順(=勝利数が多い順)に並び替える、同じなら選手番号が小さい順が上

for i in range(2 * N):
    print(rank[i][1] + 1)

勝利数カウントを+1にした場合

基本的には同じですが、ソート部分がやや面倒になります。

  # 上は省略
    if h1 == h2:  # あいこです
            continue
        else:
            winner = D[h1][h2]
            if winner == 1:
                rank[2 * i][0] += 1
            else:
                rank[2 * i + 1][0] += 1
    rank.sort(key=lambda x: x[1])  # まず選手番号が小さい順に並び替える
    rank.sort(key=lambda x: x[0], reverse=True)  # その後、勝利数が大きい順に並び替える

D問題『Between Two Arrays』

問題ページD - Between Two Arrays
コーダー正解率:5.1 %
コーダー正解率:27.2 %
コーダー正解率:75.4 %

入力

$N$ : 数列の長さ
$a_i$ : 広義単調増加な長さ $N$ の数列 $A$ の、$i$ 番目の要素
$b_i$ : 広義単調増加な長さ $N$ の数列 $B$ の、$i$ 番目の要素($a_i\le{b_i}$)

考察

動的計画法で解きます。

状態

$dp[i][c]$ : $i$ 番目まで決めて、最後が $c$ の数列の数

初期状態

$dp[0][0]=1$
$dp[i][c]=0$(それ以外)

遷移

$dp[i+1][c] = dp[i][0] + dp[i][1] + \dots + dp[i][c-1]+dp[i][c]$ ($a_{i+1}\le{c}\le{b_{i+1}}$)
$dp[i+1][c] = 0$ (それ以外の $c$ すべて)

長さ $i$ の数列に $c$ を付け加えて、長さ $i+1$ で最後が $c$ の数列にします。問題文の条件より、$a_{i+1}\le{c\le{b_{i+1}}}$ かつ $c_i\le{c_{i+1}}$ という条件を満たす数列が可能です。

実装

Pythonのコードで遷移を素直に実装すると、以下のコードになります。

dp[i+1][c] = sum(dp[i][:c+1]) % 998244353

しかし、この方法で動的計画法を実装すると、毎回sum(dp[i][:c+1]) を計算する部分の計算量が $O(C)$ ($C = 3000$)となるので、全体の計算量が $O(NC^2)$となり、TLEになります。

そこで、累積和の考えを使ってDPを高速化します。s = sum(dp[i][:c])が既に求まっていれば、sum(dp[i][:c+1]) = s + dp[i][c] という関係を使って、$O(1)$ で求めることができます。こうすることで、全体の計算量が $O(NC)$ となり間に合います。

コード

Pythonだと実行時間制限がギリギリ($1800$ ms程度)なので、PyPy($250$ ms程度)で出したほうがいいです。

def main():
    MOD = 998244353
    N = int(input())
    A = list(map(int, input().split()))
    B = list(map(int, input().split()))
    CONST = 3001

    dp = [[0] * CONST for _ in range(N + 1)]
    dp[0][0] = 1

    for i in range(N):
        a = A[i]
        b = B[i]
        s = sum(dp[i][:a]) % MOD  # dp[i]のa未満の総和です
        for c in range(a, b + 1):
            s += dp[i][c]  # sum(dp[i][:c+1])を毎回計算する代わりに、既に求まっているsum(dp[i][:c])にdp[i][c]を足します
            s %= MOD
            dp[i + 1][c] = s
    print(sum(dp[N]) % MOD)


if __name__ == '__main__':
    main()

1次元配列2つで高速化

$dp[i+1][c]$ を求めるのに必要なのは $dp[i]$ だけなので、二次元配列の代わりに一次元配列 $2$ つを使うことで少し高速になります。(Pythonで $1100$ ms程度)

def main():
    MOD = 998244353
    N = int(input())
    A = list(map(int, input().split()))
    B = list(map(int, input().split()))
    CONST = 3001

    prev = [0] * CONST
    prev[0] = 1

    for i in range(N):
        curr = [0] * CONST
        a = A[i]
        b = B[i]
        s = sum(prev[:a]) % MOD
        for c in range(a, b + 1):
            s += prev[c]
            s %= MOD
            curr[c] = s
        prev = curr
    print(sum(curr) % MOD)


if __name__ == '__main__':
    main()
12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6