【AtCoder】ABC309 – C問題について解説【Python】

abc309-c プログラミング

そまちょブログのそまちょ(@somachob)です。

この記事は、AtCoder Beginner Contest 309 の C問題についての解説です。

C – Medicine

問題へのリンク

入力例1をもとに解説します。

入力例1
4 8
6 3
2 5
1 9
4 2

入力を受け取ります。

薬については、リストで受け取っています。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

# 入力確認
print(N, K)
print(ab)

""" 出力結果
4 8
[[6, 3], [2, 5], [1, 9], [4, 2]]
"""

入力の受け取りについて、わからない場合は次の記事を参考にしてください。

シミュレーション

入力例1のとき、1日に飲む薬の数は次のようになります。

1日目2日目3日目4日目5日目6日目7日目
薬1(6日間、3錠)
薬2(2日間、5錠)
薬3(1日間、9錠)
薬4(4日間、2錠)
合計1910

飲む薬の錠数を1日目から順番に計算していき、錠数が K 以下になった日にちを解答します。

たとえば次のように実装した場合、TLE になってしまうので、計算量を減らす必要があります。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

# 薬を飲む最大の日数
max_day = 0
for medicine in ab:
    max_day = max(max_day, medicine[0])

# 1日目から最大の日数の翌日まで
for day in range(1, max_day + 2):
    # 飲む薬の錠数
    total = 0
    
    # 飲む薬の錠数を計算する
    for medicine in ab:
        # 飲み終えた薬のときはスキップ
        if day > medicine[0]:
            continue
        
        # 薬の錠数を足す
        total += medicine[1]
    
    # 飲む薬の錠数が K 錠以下のとき
    if total <= K:
        print(day)
        exit()

計算量を減らす

1日ずつ計算する必要はなく、各薬の飲む日数 ai の翌日に飲む薬の錠数が変わっています。

  • 薬1は、6日間なので、7日目で飲む薬の錠数が変わっています。
  • 薬2は、2日間なので、3日目で飲む薬の錠数が変わっています。
  • 薬3は、1日間なので、2日目で飲む薬の錠数が変わっています。
  • 薬1は、4日間なので、5日目で飲む薬の錠数が変わっています。
1日目2日目3日目4日目5日目6日目7日目
薬1(6日間、3錠)
薬2(2日間、5錠)
薬3(1日間、9錠)
薬4(4日間、2錠)
合計1910

このことから、各薬の飲む日数 ai の翌日に飲む薬の錠数だけを計算するだけでいいことがわかります。

ただし、1日目に K 錠以下かを判定する必要があるので注意が必要です。

飲む薬の錠数を計算するときは、飲み終えるのが早い薬から順番に計算する必要があります。

薬1から計算すると、飲み終える翌日の7日目の錠数について計算するので、0錠で K 錠以下のため 7 が出力されてしまいます。

飲み終えるのが早い薬から計算するためにリスト ab をソートします。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

print(ab)	# => [[6, 3], [2, 5], [1, 9], [4, 2]]

# ソート
ab.sort()

print(ab)	# => [[1, 9], [2, 5], [4, 2], [6, 3]]

これを実装したのが次のコードです。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

# 1日目が K 錠以下かを計算
# 飲む薬の錠数
total = 0

# 飲む薬の錠数を計算する
for medicine in ab:
    # 薬の錠数を足す
    total += medicine[1]

# 飲む薬の錠数が K 錠以下のとき
if total <= K:
    print(1)
    exit()


# 薬の飲む日数が小さい順に並び替え    
ab.sort()

# 各薬の飲む日数の翌日だけを計算
for medicine in ab:
    # 飲む日数の翌日
    day = medicine[0] + 1
    
    # 飲む薬の錠数
    total = 0
    
    # 飲む薬の錠数を計算する
    for medicine in ab:
        # 飲み終えた薬のときはスキップ
        if day > medicine[0]:
            continue
        
        # 薬の錠数を足す
        total += medicine[1]
    
    # 飲む薬の錠数が K 錠以下のとき
    if total <= K:
        print(day)
        exit()

これでもまだ、TLE です。

さらに計算量を減らす

薬の飲む錠数を毎回計算していましたが、実は、1日目の薬の錠数から、飲み終えた薬の錠数を引いていけば、薬の飲む錠数は計算できます。

1日目のすべての薬を飲むときの錠数は19錠です。ここから、飲み終えた薬の錠数を引いていきます。

  • 薬3は1日目に飲み終えるので、2日目は19錠ー9錠=10錠
  • 薬2は2日目に飲み終えるので、3日目は10錠ー5錠=5錠
  • 薬4は4日目に飲み終えるので、5日目は5錠ー2錠=3錠
  • 薬1は6日目に飲み終えるので、7日目は3錠ー3錠=0錠
1日目2日目3日目4日目5日目6日目7日目
薬3(1日間、9錠)
薬2(2日間、5錠)
薬4(4日間、2錠)
薬1(6日間、3錠)
合計1910

これを実装したのが次のコードです。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

# 1日目が K 錠以下かを計算
# 飲む薬の錠数
total = 0

# 飲む薬の錠数を計算する
for medicine in ab:
    # 薬の錠数を足す
    total += medicine[1]

# 飲む薬の錠数が K 錠以下のとき
if total <= K:
    print(1)
    exit()

    
# 薬の飲む日数が小さい順に並び替え    
ab.sort()

# 各薬の飲む日数の翌日だけを計算
for medicine in ab:
    # 飲む日数の翌日
    day = medicine[0] + 1
        
    # 飲み終えた薬の錠数を引く
    total -= medicine[1]
    
    # 飲む薬の錠数が K 錠以下のとき
    if total <= K:
        print(day)
        exit()

無事 AC できました。

二分探索

二分探索をすることで解くこともできます。

答えの日にちを二分探索で求めていきます。

具体的には次のように探索します。

  • K 錠以上の日を ng、K 錠以下の日を ok、K 錠以上の日と K 錠以下の日の真ん中を mid とします。
  • ng を0日目、ok を7日目(薬を飲む最大の日数の翌日)、mid を3日目とします。
  • mid の3日目が K 錠以下かを確認します。
  • 3日目は K 錠以下のため、ok が3日目になります。
  • ng を0日目、ok を3日目、mid を1日目とします。
  • mid の1日目が K 錠以下かを確認します。
  • 1日目は K 錠以上のため、ng が1日目になります。
  • これを ng と ok が隣同士になるまで繰り返します。
abc309-c_1
abc309-c_2

これで、条件を満たす日にちを求めることができます。

二分探索で実装したのが次のコードです。このコードでも AC できます。

# 入力
N, K = map(int, input().split())
ab = [list(map(int, input().split())) for _ in range(N)] 

# 飲む錠数が K 錠以下か判定する関数
def isOk(day):
    # 飲む薬の錠数
    total = 0
    
    # 飲む薬の錠数を計算する
    for medicine in ab:
        # 飲み終えた薬のときはスキップ
        if day > medicine[0]:
            continue
        
        # 薬の錠数を足す
        total += medicine[1]
        
    # 飲む薬の錠数が K 以下かを返す
    if total <= K:
        return True
    else:
        return False

      
# 二分探索の関数
def binarySearch(day):
    ng = 0
    ok = day
    
    # ok と ng が隣同士になるまで探索
    while abs(ok - ng) > 1:
        mid = (ok + ng) // 2
        if isOk(mid):
            ok = mid
        else:
            ng = mid

    # K 錠以下の日にちを返す
    return ok
    
    
# 薬を飲む最大の日数
max_day = 0
for medicine in ab:
    max_day = max(max_day, medicine[0])

# 探索の開始日
day = max_day + 1

# 二分探索の結果を表示
print(binarySearch(day)) 

参考

二分探索アルゴリズムを一般化 〜 めぐる式二分探索法のススメ 〜 – Qiita

タイトルとURLをコピーしました