E - お菓子の詰め合わせ / Assortment of Sweets Editorial by admin
Qwen3-Coder-480B概要
\(N\) 種類のお菓子の中からいくつか選び、その合計金額をちょうど \(S\) 円にする方法の数を求めよ。
考察
この問題は「部分集合の和が \(S\) になる選び方は何通りあるか?」という典型的な部分和問題の応用です。しかし、制約が厳しいため単純な全探索では間に合いません。
例えば、各お菓子について「買う」「買わない」の2択があるため、全部で \(2^N\) 通りの選び方があります。\(N\) が最大 \(40\) なので、\(2^{40} \approx 10^{12}\) となり、現実的な時間内に計算できません。
そこで登場するのが 半分全列挙 というテクニックです。配列を前半と後半の2つに分割し、それぞれの部分集合の和を全列挙します。これにより、計算量を \(O(2^N)\) から \(O(2^{N/2})\) へと削減できます。
具体的には: - 前半の部分集合の和をすべて求め、ハッシュテーブル(辞書)にその出現回数を記録します。 - 後半についても同様に行います。 - 最後に、「前半の和 + 後半の和 = \(S\)」となる組み合わせの数をカウントします。
たとえば、前半の和が \(x\) であるものが \(a\) 通りあり、後半の和が \(S - x\) であるものが \(b\) 通りあるなら、その積 \(a \times b\) だけ条件を満たす選び方があります。
このようにして、計算量を大幅に削減しながら答えを求めることができます。
アルゴリズム
- 配列 \(A\) を前半
leftと後半rightに分割する。 leftのすべての部分集合について、その要素の和を計算し、left_sumsという辞書に記録(キー:和、値:その和になる部分集合の個数)。- 同様に
rightについてもright_sumsを作成。 left_sumsの各キー \(ls\) に対して、\(S - ls\) がright_sumsに存在すれば、その積を答えに加算。- 答えを出力。
計算量
- 時間計算量: \(O(N \cdot 2^{N/2})\)
- 空間計算量: \(O(2^{N/2})\)
実装のポイント
defaultdict(int)を使うことで、キーが存在しなくてもエラーにならず初期値0として扱える。- ビット演算(bitmask)を使って部分集合を列挙している。
- 入力を高速に読み込むために
sys.stdin.readを使用している。
ソースコード
from collections import defaultdict
def main():
import sys
input = sys.stdin.read
data = input().split()
N = int(data[0])
S = int(data[1])
A = list(map(int, data[2:]))
# 半分全列挙
mid = N // 2
left = A[:mid]
right = A[mid:]
# 左半分のすべての部分集合の和を計算
left_sums = defaultdict(int)
for mask in range(1 << len(left)):
s = 0
for i in range(len(left)):
if mask & (1 << i):
s += left[i]
left_sums[s] += 1
# 右半分のすべての部分集合の和を計算
right_sums = defaultdict(int)
for mask in range(1 << len(right)):
s = 0
for i in range(len(right)):
if mask & (1 << i):
s += right[i]
right_sums[s] += 1
# 結果を計算
count = 0
for ls, lc in left_sums.items():
need = S - ls
if need in right_sums:
count += lc * right_sums[need]
print(count)
if __name__ == "__main__":
main()
この解説は qwen3-coder-480b によって生成されました。
posted:
last update: