D - お買い物チャレンジ / Shopping Challenge Editorial by admin
Claude 4.6 Opus (Thinking)概要
\(N\) 個の商品から1個以上選んで合計金額がちょうど \(S\) 円になる選び方の数 \(C\) を求め、\(C=0\), \(C=1\), \(C \geq 2\) のいずれかを判定する部分集合の和の問題です。
考察
素朴なアプローチの問題点
全ての部分集合を列挙して合計を調べる方法では、\(2^N\) 通りを調べる必要があります。\(N \leq 40\) のとき \(2^{40} \approx 10^{12}\) となり、時間制限に間に合いません。
重要な気づき
- \(N \leq 40\) という制約は、半分全列挙(Meet in the Middle) を使うサインです。
- 商品を前半・後半に分割すると、各半分は最大20個。\(2^{20} \approx 10^6\) なので十分高速に全列挙できます。
- \(C\) の正確な値は不要で、\(0\), \(1\), \(2\) 以上のどれかだけ分かればよいため、各部分集合の和の出現回数を 2で打ち切り(cap) できます。
アルゴリズム
商品を2つに分割: 前半 \(A\)(\(\lfloor N/2 \rfloor\) 個)と後半 \(B\)(残り)に分ける。
各半分の部分集合の和を列挙:
- 前半の全 \(2^{|A|}\) 通りの部分集合について合計 \(s_a\) を計算し、辞書に「合計値 → 出現回数(最大2)」を記録。
- 後半も同様に辞書を作成。
組み合わせ: 前半の各合計 \(s_a\) に対して、後半で \(S - s_a\) となる部分集合があるか辞書を参照。出現回数の積を加算して合計カウントを求める。合計が2以上になった時点で打ち切り。
空集合の扱い: mask=0(何も選ばない)は合計0を生成しますが、\(S \geq 1\) なので「前半も後半も空」のケースが \(S\) に一致することはありません。「前半が空で後半から選ぶ」「後半が空で前半から選ぶ」は1個以上選んでいるので有効です。
capの正当性:
- capped total \(\geq 2\) → 実際の total \(\geq 2\)(capは過小評価しかしないので)
- capped total \(= 1\) → 唯一の寄与は \(c_a = 1, c_b = 1\) の組。もし実際の個数がどちらか2以上なら、capされた値も2になり積が2以上になるはず。よって実際も1。
- capped total \(= 0\) → 実際も0。
計算量
- 時間計算量: \(O(2^{N/2} \cdot N/2)\)(各半分の全列挙にビットごとの加算)
- \(N=40\) のとき約 \(2^{20} \times 20 \approx 2 \times 10^7\) 程度
- 空間計算量: \(O(2^{N/2})\)(辞書に格納する部分集合の和の数)
実装のポイント
辞書のcap: 出現回数を2で打ち切ることで、巨大なカウントを扱う必要がなくなり、判定もシンプルになる。
早期終了: 組み合わせフェーズで合計が2以上になったら即座にループを抜けられる。
\(S\) の値が \(10^{18}\) と大きい: Pythonは多倍長整数を自然に扱えるため、オーバーフローの心配は不要。C++等では注意が必要。
\(N\) が小さい場合(例: \(N=1\))でも正しく動作するよう、半分の分割は \(\lfloor N/2 \rfloor\) と \(N - \lfloor N/2 \rfloor\) で行う。
ソースコード
import sys
from collections import defaultdict
def solve():
input_data = sys.stdin.read().split()
N = int(input_data[0])
S = int(input_data[1])
P = [int(input_data[i+2]) for i in range(N)]
# Meet in the middle approach
# Split into two halves
half1 = N // 2
half2 = N - half1
A = P[:half1]
B = P[half1:]
# Enumerate all subset sums for each half
# We only need to know if total count is 0, 1, or >=2, so we can cap counts at 2
def enum_sums(items):
"""Return a dict: sum -> count of subsets achieving that sum, capped at 2"""
sums = defaultdict(int)
n = len(items)
for mask in range(1 << n):
s = 0
for i in range(n):
if mask & (1 << i):
s += items[i]
if sums[s] < 2:
sums[s] += 1
# We keep going but cap at 2
return sums
def enum_sums_capped(items):
"""Return a dict: sum -> min(count, 2)"""
sums = defaultdict(int)
n = len(items)
for mask in range(1 << n):
s = 0
for i in range(n):
if mask & (1 << i):
s += items[i]
if sums[s] < 2:
sums[s] += 1
return sums
sums_a = enum_sums_capped(A)
sums_b = enum_sums_capped(B)
# For each sum_a in sums_a, we need sum_b = S - sum_a in sums_b
# But note: mask=0 means selecting nothing from that half
# We need at least 1 item total, so we can't have both masks = 0
# mask=0 gives sum=0 for both halves
# So the pair (0,0) from A and B would give sum=0 selecting 0 items - invalid
# We need to handle: total count of ways to get sum S with at least 1 item
# Count C = sum over all (mask_a, mask_b) where sum_a + sum_b = S and not (mask_a=0 and mask_b=0)
# = sum over all pairs where sum_a + sum_b = S, minus the case where both are empty (sum=0, which equals S only if S=0)
# Since S >= 1 (from constraints S >= 1), the empty+empty case (sum=0) never equals S.
# So we just compute the total.
total = 0
for sa, ca in sums_a.items():
need = S - sa
if need in sums_b:
cb = sums_b[need]
total += ca * cb
if total >= 2:
break
# But wait, we need to be more careful. ca and cb are capped at 2.
# If ca=2 and cb>=1, then ca*cb >= 2, so total >= 2.
# If ca=1 and cb=2, same.
# If ca=1 and cb=1, contribution is 1, and we need to check if there are more pairs.
# The issue is we might miss counting if we break too early but total is already >=2.
# Actually once total >= 2 we can break.
# However, we also need to exclude the case where both halves select nothing (mask=0).
# mask=0 contributes sum=0. If S=0 this would be a problem, but S>=1 so no issue.
# But actually we DO include mask_a=0 (selecting nothing from A) combined with mask_b selecting items from B.
# That's valid (at least 1 item from B). Similarly mask_b=0 with items from A. Both are fine.
if total >= 2:
# Need to recheck: could total be inflated? No, caps at 2 per side means
# if true count on one side is >=2, we record 2, which could undercount.
# But we only need to know if total >= 2, and if we get >= 2 with capped counts, real is also >= 2.
print("YES")
elif total == 0:
print("NO")
else:
# total == 1 with capped counts. Since capping can only decrease counts,
# if capped total is 1, real total is also exactly 1.
# Wait, no. Capping could make real total > capped total. But capped >= real? No, capped <= real.
# If capped total = 1, real total >= 1. Could real total be > 1?
# No: if real total were >=2, then capped total >= 2 (since each ca*cb with cap is <= real ca*real cb).
# Hmm actually that's not necessarily true. But since we cap at 2 and multiply,
# min(rc,2)*min(rb,2) >= min(rc*rb, 2) is not always true... but sum could be tricky.
# Actually since total=1, there's exactly one (sa, S-sa) pair contributing, with ca=1, cb=1.
# Real ca >= 1, real cb >= 1, so real contribution >= 1. If real ca or cb > 1, capped would be 2, giving total >=2.
# So real total = 1.
print("ALMOST")
solve()
この解説は claude4.6opus-thinking によって生成されました。
posted:
last update: