LoginSignup
12
2

More than 1 year has passed since last update.

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

Posted at

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

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

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

ご質問・ご指摘はコメントツイッターマシュマロ、Discordサーバーまでお気軽にどうぞ!

Twitter: u2dayo
マシュマロ: https://marshmallow-qa.com/u2dayo
ほしいものリスト: https://www.amazon.jp/hz/wishlist/ls/2T9IQ8IK9ID19?ref_=wl_share
Discordサーバー(質問や記事の感想・リクエストなどどうぞ!) : https://discord.gg/jZ8pkPRRMT
よかったらLGTM拡散していただけると喜びます!

目次

ABC240 まとめ
A問題『Edge Checker』
B問題『Count Distinct Integers』
C問題『Jumping Takahashi』
D問題『Strange Balls』
E問題『Ranges on Tree』

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

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

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

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

ABC240 まとめ

全提出人数: 8931人

パフォーマンス

パフォ AC 点数 時間 順位(Rated内)
200 AB------ 300 14分 5758(5525)位
400 AB------ 300 5分 4741(4508)位
600 ABC----- 600 28分 3843(3632)位
800 ABCD---- 1000 83分 2999(2792)位
1000 ABCD---- 1000 34分 2236(2031)位
1200 ABCD-F-- 1500 76分 1608(1410)位
1400 ABCDE--- 1500 46分 1127(931)位
1600 ABCDEF-- 2000 109分 753(576)位
1800 ABCDEF-- 2000 82分 487(331)位
2000 ABCDEF-- 2000 64分 302(177)位
2200 ABCDEF-- 2000 50分 182(85)位
2400 ABCDEF-- 2000 32分 109(38)位

色別の正解率

人数 A B C D E F G Ex
3034 97.9 % 93.3 % 23.1 % 18.4 % 3.0 % 0.5 % 0.0 % 0.0 %
1350 99.0 % 99.0 % 72.4 % 60.1 % 12.4 % 1.7 % 0.0 % 0.0 %
1047 97.8 % 97.5 % 93.7 % 89.4 % 51.4 % 8.0 % 0.2 % 0.0 %
674 98.2 % 98.2 % 97.9 % 96.6 % 86.5 % 37.5 % 1.2 % 0.1 %
394 98.0 % 98.2 % 98.2 % 97.2 % 98.0 % 76.7 % 6.1 % 0.0 %
178 83.7 % 84.3 % 84.3 % 85.4 % 85.4 % 75.8 % 25.3 % 2.2 %
42 83.3 % 83.3 % 83.3 % 83.3 % 81.0 % 76.2 % 52.4 % 7.1 %
20 95.0 % 100.0 % 100.0 % 100.0 % 100.0 % 100.0 % 60.0 % 25.0 %

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

A問題『Edge Checker』

問題ページA - Edge Checker
コーダー正解率:97.9 %
コーダー正解率:99.0 %
コーダー正解率:97.8 %

入力

$a,b$ : $2$ 点の番号

  • $a\lt{b}$

考察

$a<b$ なので、基本的には $b-a=1$ ならば隣あっていますが、$a=1,b=10$ のケースだけはこれに当てはまりません。

$a=1,b=10$ のパターンだけ特別に判定するといいでしょう。

コード

a, b = map(int, input().split())
if b - a == 1 or (a, b) == (1, 10):
    print("Yes")
else:
    print("No")

B問題『Count Distinct Integers』

問題ページB - Count Distinct Integers
コーダー正解率:93.3 %
コーダー正解率:99.0 %
コーダー正解率:97.5 %

入力

$N$ : 正整数列 $a$ の長さ
$a_i$ : 正整数列 $a$ の $i$ 番目の要素

  • $1\le{a_i}\le{10^9}$

考察

len(set(A)) で、リストやタプル A に含まれる要素の種類数が一発でわかります。

コード

N = int(input())
A = list(map(int, input().split()))
print(len(set(A)))

C問題『Jumping Takahashi』

問題ページC - Jumping Takahashi
コーダー正解率:23.1 %
コーダー正解率:72.4 %
コーダー正解率:93.7 %

入力

$N$ : ジャンプの回数
$X$ : 目的の座標($X\le{10000}$)
$a_i,b_i$ : $i$ 回目のジャンプでは、$a_i$ か $b_i$ 正の方向に移動する

考察

動的計画法(DP)で解きます。

状態

$\mathrm{dp}[i][j]$ : $i$ 回ジャンプを行った時点で、座標 $j$ にいることができるなら True、できないなら False

初期値は、$\mathrm{dp}[0][0]$ のみ True で、ほかはFalseです。($1$ 度もジャンプしていない時点では、座標 $0$ に存在するため)

正の方向にしかジャンプできませんから、座標 $X+1$ 以降からは絶対に座標 $X$ に到達不可能です。したがって、配列の二次元目は $X$ までで良いです。

遷移

$\mathrm{dp}[i][j]$ が True であれば、$i+1$ 回目のジャンプで

  • 座標 $j$ から $a_i$ 正の方向にジャンプすることで座標 $j+a_i$ に移動し $\mathrm{dp}[i+1][j+a_i]$ を True
  • 座標 $j$ から $b_i$ 正の方向にジャンプすることで座標 $j+b_i$ に移動し $\mathrm{dp}[i+1][j+b_i]$ を True

することができます。

ジャンプしないという選択はできませんから、$\mathrm{dp}[i][j]$ が True でも、$\mathrm{dp}[i+1][j]$ が Trueになるとは限らないことに注意してください。

コード

def solve():
    N, X = map(int, input().split())
    dp = [[False] * (X + 1) for _ in range(N + 1)]
    dp[0][0] = True

    for i in range(N):
        a, b = map(int, input().split())
        for j in range(X + 1):
            if dp[i][j]:
                if j + a <= X:
                    dp[i + 1][j + a] = True
                if j + b <= X:
                    dp[i + 1][j + b] = True
    return dp[N][X]


print("Yes" if solve() else "No")

setを使う方法

$2$ つのセットを使っても実装できます。なお、 $a_i,b_i$ の制約が小さいので、$X+1$ 以上の座標を捨てずに持っていても実は問題ありません。

def solve():
    N, X = map(int, input().split())
    pre = {0}

    for _ in range(N):
        a, b = map(int, input().split())
        cur = set()
        for x in pre:
            cur.add(x + a)
            cur.add(x + b)
        pre = cur
    return X in cur


print("Yes" if solve() else "No")

多倍長整数を使う方法

整数を二進法表記したときの $i$ 桁目が $1$ なら $i$ が True、 $0$ なら False とみなして、ビット演算でDPを行います。

遷移は $a_i$ ビット、 $b_i$ ビット左シフトしたものの論理和を取ればいいです。

C++でいうところのbitsetDPです。難しめの問題だと、この手法による高速化が必要になる場合もあります。

def solve():
    N, X = map(int, input().split())
    dp = 1
    for _ in range(N):
        a, b = map(int, input().split())
        dp = (dp << a) | (dp << b)
    return dp >> X & 1


print("Yes" if solve() else "No")

D問題『Strange Balls』

問題ページD - Strange Balls
コーダー正解率:18.4 %
コーダー正解率:60.1 %
コーダー正解率:89.4 %

入力

$N$ : ボールの数
$a_i$ : $i$ 回目に筒に落とすボールには、$a_i$ が書かれている($a_i$ は $2$ 以上の整数)

考察

スタックというデータ構造を使って解きます。スタックは、後入れ先出し(LIFO)の構造でデータを保持するものです。Pythonでは、普通のリストでスタックを実現できます。単に入れるときはappend、出すときはpopを使うだけです。

たとえば、入力例 $1$ の $\{3,2,3,2,3\}$ をリストでシミュレートすると、リストは以下のように変化します。

[]
3:[3]
2:[3, 2]
3:[3, 2, 3]
2:[3, 2, 3, 2]
3:[3, 2, 3, 2, 2] -> [3, 2, 3]

しかし、$k$ が書かれたボール を入れるたびに、リストの後ろから $k-1$ 個を確認してボールが消えるか判定していては、計算量が $O(N^2)$ となるため間に合いません。

そこで、そのままリストに数字を入れるのではなく、$(ボールに書かれた整数、ボールの連続する個数)$ の形でリストに入れます。こうすれば、筒の一番上でどの整数が何個連続しているかを、リストの一番後ろを確認するだけで済みます。

入力例 $2$ の $\{2,3,2,3,3,3,2,3,3,2\}$ をこの形式でシミュレートすると、リストは以下のように変化します。

2: [[2, 1]]
3: [[2, 1], [3, 1]]
2: [[2, 1], [3, 1], [2, 1]]
3: [[2, 1], [3, 1], [2, 1], [3, 1]]
3: [[2, 1], [3, 1], [2, 1], [3, 2]]
3: [[2, 1], [3, 1], [2, 1], [3, 3]]
-> [[2, 1], [3, 1], [2, 1]]
2: [[2, 1], [3, 1], [2, 2]]
-> [[2, 1], [3, 1]]
3: [[2, 1], [3, 2]]
3: [[2, 1], [3, 3]]
-> [[2, 1]]
2: [[2, 2]]
-> []

コード

def main():
    N = int(input())
    A = list(map(int, input().split()))
    L = [[-1, 0]]  # 番兵として絶対取り出されない要素を1個入れておくと楽です
    ans = 0
    for cx in A:
        tx, cnt = L[-1]
        ans += 1  # ボールを1個入れる
        if cx == tx:  # 筒の一番上と同じ数の場合、連続数を+1
            L[-1][1] += 1
        else:  # 違う数なら、[cx, 1]を筒の一番上に追加
            L.append([cx, 1])
        if L[-1][0] == L[-1][1]:  # 筒の一番上で、cxがcx個連続したら、消す
            L.pop()
            ans -= cx  # ボールがcx個消えたので減らす
        print(ans)


main()

E問題『Ranges on Tree』

問題ページE - Ranges on Tree
コーダー正解率:3.0 %
コーダー正解率:12.4 %
コーダー正解率:51.4 %

入力

$N$ : 木の頂点数
$u_i,v_i$ : $i$ 本目の辺は頂点 $u_i$ と $v_i$ を結ぶ

考察

葉からボトムアップに考えます。

葉はL_u=R_u

どの $2$ つの葉を選んでも、当然ですが部分木の集合に共通部分はありません。(葉の部分木はその葉の頂点 $1$ 個だけからなるため)

そのため、葉同士の区間は交わらないようにする必要があります。$L,R$ の 最大値を最小にしたいので、葉は $(L_u,R_u)=(1,\ 1), (2,\ 2), (3,\ 3),\dots$ と $1$ から順に $L_u=R_u$ となるようにして値を節約します。

親のL_uは子の最小、R_uは子の最大

次に、葉の $1$ 階層上の頂点の $L_u,R_u$ を決めます。これは、以下のように決定することで無駄なく、$L_u$ ~ $R_u$ ですべての葉頂点と共通区間があるようにできます。

  • $L_u=\mathrm{min}({L_v\ |\ v\ は\ u\ の子 })$
  • $R_u=\mathrm{max}({R_u\ |\ v\ は\ u\ の子 })$

さらに上の階層の頂点も、同様にして決められます。

最終的に、例えば葉の数が $9$ 個だとすると、$L_1,R_1=\ (1,9)$ となります。

図解

言葉だけだとわかりづらいと思うので、図を用意しました。

  • 赤字は $L_i$
  • 青字は $R_i$
  • 葉頂点は緑でマークしてある
  • 緑の下線がそれぞれ親の $L_u,R_u$ に採用された最小・最大の $L_u,R_u$

abc240e.png

対応する入力

16
1 2
1 3
2 4
2 5
3 6
4 7
4 8
6 9
6 10
8 11
8 12
10 13
10 14
10 15
10 16

対応する出力

後述のコードの出力は以下になります。図と同じことが確認できます。(子を探索する順番によって数字が入れ替わる場合がありますが、いずれも正解です)

1 9
1 4
5 9
1 3
4 4
5 9
1 1
2 3
5 5
6 9
2 2
3 3
6 6
7 7
8 8
9 9

実装

解説のアルゴリズムを、深さ優先探索(DFS)で実装すればいいです。

コード

PyPyは再帰に弱いので、Pythonで出してください。

import sys

sys.setrecursionlimit(10 ** 6)  # 再帰上限を上げないとREになります
readline = sys.stdin.readline  # 一応高速な入力を使っておきましょう(readlineは改行文字も受け取るので注意)


def main():
    leaf_count = 0  # これまでの葉の数

    def dfs(u, p):
        nonlocal leaf_count
        is_leaf = True
        l_min = r_max = leaf_count + 1
        for v in G[u]:
            if v == p:
                continue
            is_leaf = False  # 子頂点があるので、葉ではない
            dfs(v, u)
            l_min = min(l_min, L[v])
            r_max = max(r_max, R[v])
        if is_leaf:
            leaf_count += 1
        L[u], R[u] = l_min, r_max

    N = int(readline())
    G = [[] for _ in range(N + 1)]
    for _ in range(N - 1):
        a, b = map(int, readline().split())
        G[a].append(b)
        G[b].append(a)
    L = [0] * (N + 1)
    R = [0] * (N + 1)
    dfs(1, 0)
    for l, r in zip(L[1:], R[1:]):
        print(l, r)


main()
12
2
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
2