E - 調合と目標量 / Mixing and Target Amount 解説 by admin
GPT 5.4 High概要
各薬品について「使わない・通常モード・加熱モード」の \(3\) 通りの選択があります。
全探索は \(3^N\) 通りで間に合わないため、薬品を半分ずつに分けて全列挙する 半分全列挙(Meet-in-the-Middle) で解きます。
考察
この問題では、各薬品ごとに次の \(3\) 通りの選択があります。
- 使わない
- 通常モードで使う(\(A_i\) mL)
- 加熱モードで使う(\(B_i\) mL)
したがって、全体の組み合わせ数は \(3^N\) 通りです。
しかし \(N \leq 26\) なので、
\[ 3^{26} \approx 2.5 \times 10^{12} \]
となり、素直に全部試すのは不可能です。
また、よくある「和がちょうど \(K\) になるか」を判定する問題なので DP を考えたくなりますが、この問題では
\[ K \leq 10^{15} \]
と非常に大きいため、和を添字にした DP は使えません。
そこで有効なのが 半分全列挙 です。
薬品を前半と後半に分けます。
それぞれの半分について「作れる合計量」をすべて列挙すれば、
- 前半で作れる量を \(x\)
- 後半で作れる量を \(y\)
として、
\[ x + y = K \]
となる組があるかを調べればよいです。
例えば \(N=26\) を \(13\) 個と \(13\) 個に分けると、それぞれの組み合わせ数は最大でも
\[ 3^{13} = 1594323 \]
程度です。
これは現実的な大きさなので、片方を列挙して集合に入れ、もう片方を列挙しながら補数 \(K-y\) があるか確認すれば判定できます。
さらに、この問題では \(A_i, B_i \geq 1\) なので、途中の合計がすでに \(K\) を超えたなら、その先でちょうど \(K\) に戻ることはありません。
そのため、列挙中に
- \(s + A_i \leq K\)
- \(s + B_i \leq K\)
のときだけ次へ進めばよく、不要な探索を減らせます。
アルゴリズム
- 薬品列を前半
leftと後半rightに分ける。 - 前半について、各薬品に対して
- 使わない
- \(A_i\) を足す
- \(B_i\) を足す
の \(3\) 通りを全列挙し、作れる合計量を
left_sumsという集合に入れる。
- 後半についても同様に全列挙する。
- 後半で合計量 \(s\) を作れたとき、前半で \(K-s\) を作れるなら合計でちょうど \(K\) になる。
つまり
$\( K - s \in \text{left\_sums} \)$
なら答えは Yes。
5. 最後まで見つからなければ No。
このコードでは再帰ではなく、スタックを使った DFS で全列挙しています。
状態は
- 今どの薬品まで見たか
- 現在の合計量
の 2 つです。
各状態からの遷移は次の 3 通りです。
- その薬品を使わない
- 通常モードで使う
- 加熱モードで使う
イメージ
例えば前半で作れる量が
\[ \{0, 3, 5, 8\} \]
だったとします。
後半を列挙していて \(s=7\) が作れたなら、
\[ K-s \]
が前半に存在するかを見ます。
もし \(K=15\) なら \(15-7=8\) なので、前半の \(8\) と後半の \(7\) を合わせてちょうど \(15\) が作れます。
計算量
- 時間計算量: \(O\left(3^{\lfloor N/2 \rfloor} + 3^{\lceil N/2 \rceil}\right)\)
- 空間計算量: \(O\left(3^{\lfloor N/2 \rfloor}\right)\)
\(N \leq 26\) なので、最大でもおおよそ \(3^{13}\) 程度の全列挙を 2 回行うだけで済みます。
実装のポイント
各薬品には「使わない」選択肢もあるので、初期状態の合計 \(0\) を必ず含めます。
そのため \(K=0\) の場合も正しく判定できます。合計が \(K\) を超えた状態は、それ以降すべて正の量しか足されないので探索不要です。
前半の合計量は
setに入れることで、重複を自動で除けます。再帰でも書けますが、コードではスタックを使って反復的に DFS をしています。これにより再帰の深さを気にせず実装できます。
ソースコード
import sys
def main():
input = sys.stdin.readline
N, K = map(int, input().split())
AB = [tuple(map(int, input().split())) for _ in range(N)]
m = N // 2
left = AB[:m]
right = AB[m:]
left_sums = set()
stack_i = [0]
stack_s = [0]
llen = len(left)
while stack_i:
i = stack_i.pop()
s = stack_s.pop()
if i == llen:
left_sums.add(s)
continue
a, b = left[i]
ni = i + 1
stack_i.append(ni)
stack_s.append(s)
x = s + a
if x <= K:
stack_i.append(ni)
stack_s.append(x)
y = s + b
if y <= K:
stack_i.append(ni)
stack_s.append(y)
stack_i = [0]
stack_s = [0]
rlen = len(right)
while stack_i:
i = stack_i.pop()
s = stack_s.pop()
if i == rlen:
if K - s in left_sums:
print("Yes")
return
continue
a, b = right[i]
ni = i + 1
stack_i.append(ni)
stack_s.append(s)
x = s + a
if x <= K:
stack_i.append(ni)
stack_s.append(x)
y = s + b
if y <= K:
stack_i.append(ni)
stack_s.append(y)
print("No")
if __name__ == "__main__":
main()
この解説は gpt-5.4-high によって生成されました。