E - 調合と目標量 / Mixing and Target Amount 解説 by admin
Claude 4.6 Opus (Thinking)概要
\(N\) 種類の薬品それぞれについて「使わない」「通常モード」「加熱モード」の3択を行い、得られる溶液の合計がちょうど \(K\) mL になるかを判定する問題です。半分全列挙(Meet in the Middle) で効率的に解きます。
考察
素朴なアプローチとその限界
各薬品には「使わない」「通常モード(\(A_i\) mL)」「加熱モード(\(B_i\) mL)」の3つの選択肢があります。\(N\) 個の薬品すべてについて全探索すると、組み合わせ数は \(3^N\) 通りです。
- \(N = 26\) のとき、\(3^{26} \approx 2.5 \times 10^{12}\) 通りとなり、到底間に合いません。
重要な気づき:半分に分ける
\(N\) 個の薬品を前半と後半の2グループに分けると、それぞれ約 \(N/2 = 13\) 個になります。
- \(3^{13} = 1{,}594{,}323\) 通り
これなら各グループの全列挙は十分高速です。前半グループで作れる合計値の集合と、後半グループで作れる合計値の集合をそれぞれ求め、「前半の値 + 後半の値 = \(K\)」となるペアが存在するかを確認すればよいのです。
具体例
\(N=4\), \(K=10\), 薬品が \((3,5), (2,4), (1,6), (7,3)\) のとき:
- 前半 \(\{(3,5), (2,4)\}\) → 合計の候補: \(\{0, 2, 3, 4, 5, 7, 8, 9\}\)
- 後半 \(\{(1,6), (7,3)\}\) → 合計の候補: \(\{0, 1, 3, 4, 6, 7, 8, 9, 10, 13\}\)
前半から \(7\) を取り、後半から \(3\) を取れば \(7 + 3 = 10 = K\) なので Yes です。
アルゴリズム
- \(N\) 個の薬品を前半(\(\lfloor N/2 \rfloor\) 個)と後半(残り)に分割する。
- 前半のすべての薬品について3択を全列挙し、可能な合計値の集合
left_sumsを作る。 - 後半も同様に全列挙し、可能な合計値の集合
right_sumsを作る(Pythonのsetに格納)。 left_sumsの各要素 \(l\) について、\(K - l\) がright_sumsに含まれるかを \(O(1)\) で判定する。- 一つでも見つかれば
Yes、見つからなければNoを出力する。
列挙の際は、現在の集合に対して各薬品ごとに「そのまま(使わない)」「\(+A_i\)(通常モード)」「\(+B_i\)(加熱モード)」の3パターンを加えていくことで、逐次的に候補を生成します。
計算量
- 時間計算量: \(O(3^{N/2})\)
- 前半・後半それぞれ \(3^{N/2}\) 個の候補を列挙し、マッチングは
setのルックアップにより各 \(O(1)\) で行えます。\(N = 26\) のとき約 \(3^{13} \approx 1.6 \times 10^6\) で十分高速です。
- 前半・後半それぞれ \(3^{N/2}\) 個の候補を列挙し、マッチングは
- 空間計算量: \(O(3^{N/2})\)
- 各グループの合計値候補を
setに保持するため。
- 各グループの合計値候補を
実装のポイント
列挙の実装:
setを使い、薬品を1つずつ処理して候補を拡張していくのが簡潔です。\(K\) の値が最大 \(10^{15}\): Python は多倍長整数を標準でサポートしているため、オーバーフローの心配はありません。
\(0\) 個選ぶ場合: 初期値として合計 \(0\) を含めておくことで自然に対応できます(\(K = 0\) のときは何も選ばない場合が該当)。
前半・後半の分割: なるべく均等に分けることで、列挙数の最大値を最小化できます。
ソースコード
import sys
from collections import defaultdict
def solve():
input_data = sys.stdin.buffer.read().split()
N = int(input_data[0])
K = int(input_data[1])
items = []
for i in range(N):
a = int(input_data[2 + 2*i])
b = int(input_data[3 + 2*i])
items.append((a, b))
# Meet in the middle approach
# Each item has 3 choices: not used (0), normal mode (A_i), heated mode (B_i)
# Split into two halves
half1 = N // 2
half2 = N - half1
def enumerate_sums(item_list):
"""Enumerate all possible sums from a list of items."""
sums = {0}
for a, b in item_list:
new_sums = set()
for s in sums:
new_sums.add(s) # don't use this item
new_sums.add(s + a) # normal mode
new_sums.add(s + b) # heated mode
sums = new_sums
return sums
left_items = items[:half1]
right_items = items[half1:]
left_sums = enumerate_sums(left_items)
right_sums = enumerate_sums(right_items)
# Check if any left_sum + right_sum == K
# Store right_sums in a set, then for each left_sum check if K - left_sum is in right_sums
right_set = right_sums
for ls in left_sums:
if K - ls in right_set:
print("Yes")
return
print("No")
solve()
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: