LoginSignup
1
0

ABC318をPythonで解いてみたよ。(A~E問題)

Last updated at Posted at 2023-09-02

AtCoder Beginners Contest 318 (ABC318) をPythonで解きました。
見やすいコードを書く練習も兼ねてます。

A - Full Moon

問題ページ

考察

変数 $ans$ を用意して、最初は $ans=0$ とします。
$M$ 日目、 $M+P$ 日目、 $M+2P$ 日目、$\cdots$ と見ていきます。
もし今見ている日にちが $N$ 以下のときは、変数 $ans$ を $1$ だけ増やして、次の日にちを見に行きます。
もし今見ている日にちが $N$ を超えていたら、日にちを $P$ ずつ増やしていた操作をbreak文などで打ち切って、変数 $ans$ を出力します。
下のコードはwhile文をつかっています。別解にもその他の書き方があるので、参考にしてみてください。

コード

N, M, P = map(int, input().split())

ans = 0
while M <= N:
    ans += 1
    M += P
print(ans)

別解

for文をつかった解法 for文をつかって、 $M$ 日目、 $M+P$ 日目、 $M+2P$ 日目、 $\cdots$ と日付を見ていきます。 rangeの引数には、どうやっても見ることのないくらい大きな数字を入れておき、見ている日にちが $N$ を超えた時点でbreak文をつかってこのループ処理を打ち切ります。
# for文をつかった解法
N, M, P = map(int, input().split())
ans = 0
for i in range(5000000):
    day = M + P * i
    if day <= N:
        ans += 1
    else:
        break
print(ans)
range関数の第3引数をつかった解法

range関数は、たとえば「 $3$ から $14$ までの整数を $3$ 個とばしで見る」みたいなことができます。

指定するのは $3$ つで、「スタートの数」「おわりの数」「何個とばしでみるか」です。「おわりの数」は、本来の数から $1$ だけ足しておかないといけません。

つかいかたはこんな感じ。

# 3から14までの整数を3個とばしで見る。
for i in range(3, 15, 3):
    print(i)  # 3, 6, 9, 12 が順に出力される

# 5から22までの整数を4個とばしで見る。
for i in range(5, 23, 4):
    print(i)  # 5, 9, 13, 17, 21 が順に出力される

この性質を用いて、今回の問題を解くことができます。

# range関数の第3引数をつかった解法
N, M, P = map(int, input().split())
ans = 0
for day in range(M, N + 1, P):
    ans += 1
print(ans)
len関数をつかった解法(発展) 上の「range関数の第3引数をつかった解法」の発展バージョンです。

今回の問題では、何回ループするかの情報がほしいだけです。これは実際にfor文をつかってループさせなくても、len関数をつかって取得できます。

# len関数をつかった解法
N, M, P = map(int, input().split())
ans = len(range(M, N + 1, P))
print(ans)
こっちで計算する解法(発展) for文やwhile文をつかってPythonにループ処理を頑張ってもらってもいいですが、自分で計算することもできます。 スタートの日にちが入力で与えられる $M$ によって毎回変わってしまうので、最初の日にちを $0$ 日目で統一するために、次のような変更をします。
  • 満月を見られる最初の日は $M$ 日目 → 満月を見られる最初の日は $0$ 日目
  • $1$ 日目から $N$ 日目の中で、満月を見られる日の数を求める → $0$ 日目から $N-M$ 日目の中で、満月を見られる日の数を求める

本来は今日がスタートだったのを $M$ 日後の今日に置き換えて、 1-indexedを0-indexedになおしました。
たとえば、「満月は $5$ 日ごとに現れる( $P=5$ )とき、 $0$ 日目から $28$ 日目の中で、満月を見られる日の数を求めてください。ただし、 $0$ 日目に満月が見られるとします。」という問題のとき、満月が見られるのは $0$ 日目、 $5$ 日目、 $10$ 日目 、 $15$ 日目、 $20$ 日目、 $25$ 日目の $6$ 回です。これは、$28//5 + 1=6$ として求めることもできます(//は小数点以下を切り捨てる割り算)。

これをそのままコードにすることで解くことができます。
$N<M$ のときは答えが $0$ になるので注意しましょう。

# こっちで計算する解法
N, M, P = map(int, input().split())
ans = (N - M) // P + 1
if ans < 0:
    ans = 0
print(ans)

B - Overlapping sheets

問題ページ

考察

$101 \times 101$ のマスを用意して、シートで覆った部分はTrue、そうでない部分はFalseということにします。
$N$ 枚のシートが与えられるので、それぞれのシートについて、覆った部分をTrueにしていきます。
$101 \times 101$ のマスの中にあるTrueの数がそのまま答えになります。

コード

N = int(input())
Grid = [[False] * 101 for _ in range(101)]
for _ in range(N):
    a, b, c, d = map(int, input().split())
    for i in range(a, b):
        for j in range(c, d):
            Grid[i][j] = True
ans = 0
for i in range(101):
    for j in range(101):
        if Grid[i][j]: ans += 1
print(ans)

C - Blue Spring

問題ページ

考察

1日周遊パスはどのタイミングでつかってもいいです。
また気持ち的には、通常料金の高い日に1日周遊パスをつかって、通常料金の安い日は1日周遊パスはつかわないようにしたいです。

なので、リスト $F$ で与えられる各日にちの通常料金のリストを、大きい順(降順)にソートします。サンプル1だと、$F=[7,1,6,3,6]$ だったのが、 $F=[7,6,6,3,1]$ になります。

最初の $D$ 日間を見てみます。
もし最初の $D$ 日間は1日周遊パスをつかった方がいいなら、$D$ 枚セットの1日周遊パスを買って、最初の $D$ 日間につかうことにします。
サンプル1だと、 $F=[7,6,6,3,1]$ で、1日周遊パスは $2$ 枚セットで $10$ 円です。最初の $2$ 日間は何もしないと $13$ 円かかってしまうので、$2$ 枚セットの1日周遊パスを $10$ 円で購入します。
これを次の $D$ 日間、さらに次の $D$ 日間、 $\cdots$ と繰り返していき、実際にかかるお金がいくらかを計算すればokです。

下のコードでは、 $D$ 枚セットの1日周遊パスを買ってつかうとき、 $D$ 日間のうち最初の日だけ $P$ 円(=1日周遊パスの値段)、それ以外の日は $0$ 円としています。

コード

N, D, P = map(int, input().split())
F = list(map(int, input().split()))
F.sort(reverse=True)

for left in range(0, N, D):
    right = min(left + D, N)
    cost = sum(F[left:right])
    if cost > P:
        for i in range(left, right):
            F[i] = 0
        F[left] = P
    else:
        break
ans = sum(F)
print(ans)

D - General Weighted Max Matching

問題ページ

考察

考えうる辺の結び方は、 $15 \times 13 \times 11 \times 9 \times 7 \times 5 \times 3 = 2027025$ 通りで、これなら全探索できそうです。
DFS全探索で解きます。

再帰関数をつかうので、次の4行を書いておくようにしましょう。

import sys
import pypyjit

sys.setrecursionlimit(10**7)
pypyjit.set_param('max_unroll_recursion=-1')

$N$ が奇数のとき、浮いちゃう頂点が出てきてややこしいので、頂点を1つ増やしてその頂点から出ている辺はすべて重みを $0$ とします。

  • $lst[i]$ : 辺の番号が2つ格納されたリスト

となるリスト $lst$ を全パターンつくります。
以下、どのようにつくるかを書いていきます。

格納する辺の番号を $v$ 、つくってる途中のリストを $lst$ とします。このことを $dfs(lst,v)$
と表記します。
$lst$ を左から順に見ていきます。
要素数が $2$ のとき、その2頂点をつなぐ予定なので、そこには $v$ は入れません。
要素数が $0$ のとき、そこに $v$ を入れた新たなリスト $nexlst$ を用意し、$dfs(nexlst, v+1)$ を考えることにします。 ただし、もう要素数が $0$ のところに $v$ は入れません。
要素数が $1$ のとき、そこに $v$ を入れた新たなリスト $nexlst$ を用意し、$dfs(nexlst, v+1)$ を考えることにします。

これで全パターンの $lst$ を列挙することができます。

コード

import sys
import pypyjit

inputs = sys.stdin.readline
sys.setrecursionlimit(10 ** 7)
pypyjit.set_param('max_unroll_recursion=-1')

N = int(inputs())
D = []
for i in range(N - 1):
    row = list(map(int, inputs().split()))
    D.append([0] * (i + 1) + row)
D.append([0] * N)
for i in range(N):
    for j in range(N):
        if i != j and D[i][j] == 0:
            D[i][j] = D[j][i]

if N % 2 == 1:
    for i in range(N):
        D[i].append(0)
    D.append([0] * (N + 1))
    N += 1

ans = 0


def dfs(lst, v):
    if v == N:
        global ans
        now_ans = 0
        for u, v in lst:
            now_ans += D[u][v]
        ans = max(ans, now_ans)
        return

    append_to_zero = False  # 過去に要素数0のところにvを追加したことがあればTrue
    for i in range(len(lst)):
        match len(lst[i]):
            case 2:
                continue
            case 0:
                if append_to_zero:
                    break
                append_to_zero = True
        nex_lst = [t[:] for t in lst]

        nex_lst[i].append(v)
        dfs(nex_lst, v + 1)


dfs([[] for _ in range(N // 2)], 0)
print(ans)

別解

bitDPをつかった解法 $0$ ~ $2^N - 1$ の合計 $2^N$ 個の数を用意します。これらの数は、「2進数で表したときに下から $i$ 桁目が $1$ のとき、頂点 $i$ はすでに他の頂点と結ばれている」という状態を表すことにします。
  • $dp[S]$ : グラフの状態が $S$ のときの、辺の重みの総和の最大値

となるdpを考えます。
最初は $dp[0] = 0$ です。

以下、下のコードで読み解くのがしんどくなりそうなところを解説します。

グラフの状態を表す整数 $S$ について、頂点 $i$ がすでに他の頂点と結ばれているかどうかは、次のように判定できます。

if (S >> i) & 1 == 1:
    print('頂点iは他の頂点と結ばれている')
elif (S >> i) & 1 == 0:
    print('頂点iはどの頂点とも結ばれていない')

また、もとのグラフの状態が $S$ で、そこに頂点 $a,b$ を結ぶ辺を追加するのは、次のように書けます。

S |= 1 << a
S |= 1 << b

itertools.combinations()をつかうと、あるリストから組み合わせのパターンをすべて列挙することができます。

from itertools import combinations

lst = [5, 12, 3, -92]
for a, b in combinations(lst, 2):
    print(a, b)

# 出力結果
# 5 12
# 5 3
# 5 -92
# 12 3
# 12 -92
# 3 -92
# bitDPをつかった解法
from collections import deque
from itertools import combinations

N = int(input())
D = []
for i in range(N - 1):
    row = list(map(int, input().split()))
    D.append([0] * (i + 1) + row)
D.append([0] * N)
for i in range(N):
    for j in range(N):
        if i != j and D[i][j] == 0:
            D[i][j] = D[j][i]

dp = [0] * (1 << N)
que = deque()
que.append(0)
while que:
    state = que.popleft()
    P = [i for i in range(N) if (state >> i) & 1 == 0]  # つかえる頂点のリスト
    if len(P) >= 2:
        for a, b in combinations(P, 2):
            nex_state = state
            nex_state |= 1 << a
            nex_state |= 1 << b
            if dp[nex_state] == 0:
                que.append(nex_state)
            dp[nex_state] = max(dp[nex_state], dp[state] + D[a][b])
print(max(dp))

E - Sandwiches

問題ページ

考察 (パート1)

次の問題を考えます。

長さ $N$ の整数列 $A=(A_1,A_2, \cdots ,A_N)$ が与えられます。以下の条件をすべて満たす正整数組 $(i,j,k)$ の個数を求めてください。

  • $1 \leq i < j < k \leq N$
  • $A_i = A_k$

もとの問題文から、$A_j \neq A_k$ の条件を取り除いただけです。この問題をいったん解いてみます。

サンプル3の $A=(9, 7, 11, 7, 3, 8, 1, 13, 11, 11, 11, 6, 13)$ を考えてみます。
たとえば $k=10$ (右から $4$ 番目)のときを考えます。
$A_i=A_{10}$ となる $i$ は、 $i=3,9$ です。
$(i,k)=(3,10)$ のとき、$j=4,5,6,7,8,9$ で $6$ 通りです。これは、 $k-i-1=6$ で算出できます。
$(i,k)=(9,10)$ のときは、$0$ 通りです。

次に $k=11$ のときを考えます。
$A_i=A_{11}$ となる $i$ は、 $i=3,9,10$ です。
$(i,k)=(3,11)$ のとき、 $k-i-1=7$ で $7$ 通りです。
$(i,k)=(9,11)$ のとき、 $k-i-1=1$ で $1$ 通りです。
$(i,k)=(10,11)$ のときは、$0$ 通りです。

今回はもう値が $11$ の場所がこの右側にないですが、もし右側にまだまだ $11$ の値があったら、そのたびに $3$ や $9$、 $10$ を $k$ から引くことになってしまいます。
毎回計算しなくても、 $k \times (左側で11の出た回数) - (11が出てきたインデックスの総和)$ で求められます。
この計算をするときに必要なのは、今までに左側で何回 $11$ が出てきたか、$11$ が出てきたインデックスの総和、の2つだけです。
なので、次の2つのリストを用意します。

  • $cnt[i]$ : 数字 $i$ が登場した回数
  • $value[i]$ : 数字 $i$ が登場したインデックスの総和

この2つのリストを管理しながら、$k=1$ から $k=N$ まで計算していきます。
これで、最初につくった問題を解くことができました。

考察 (パート2)

次の問題を考えます。

長さ $N$ の整数列 $A=(A_1,A_2, \cdots ,A_N)$ が与えられます。以下の条件をすべて満たす正整数組 $(i,j,k)$ の個数を求めてください。

  • $1 \leq i < j < k \leq N$
  • $A_i = A_k$
  • $A_j = A_k$

もとの問題文の$A_j \neq A_k$ の条件を $A_j=A_k$ に変えました。
パート1での問題の答えから、この問題の答えを引くことで、もとの問題の答えになります(補集合の考え方です)。

$A_i$ と同じ値の数を $c$ とすると、その数について $A_i=A_j=A_k$ になるパターンの数は、$_cC_3=c(c-1)(c-2)/6$ です。

これで、もとの問題を解くことができました。

コード

N = int(input())
A = list(map(lambda a: int(a) - 1, input().split()))

ans = 0
cnt = [0] * N  
value = [0] * N
for i, a in enumerate(A):
    c = cnt[a]
    v = value[a]
    if c >= 1:
        ans += i * c - v - c
    cnt[a] += 1
    value[a] += i

for c in cnt:
    if c <= 2:
        continue
    ans -= c * (c - 1) * (c - 2) // 6

print(ans)
1
0
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
1
0