C - おやつの分配 / Snack Distribution Editorial by admin
Claude 4.6 Opus (Thinking)概要
\(N\) 個のお菓子を2人に分配する際、それぞれが1個以上受け取る条件の下で、美味しさの合計の差の絶対値を最小化する問題です。部分集合の和に関する動的計画法(DP)で解きます。
考察
問題の言い換え
全体の美味しさの合計を \(S\) とすると、高橋君が受け取るお菓子の美味しさの合計を \(T\) としたとき、青木君の合計は \(S - T\) です。差の絶対値は \(|T - (S - T)| = |2T - S|\) となります。
つまり、\(N\) 個のお菓子から1個以上 \(N-1\) 個以下を選んだ部分集合の和 \(T\) について、\(|2T - S|\) を最小化する問題に帰着できます。
素朴なアプローチ
全ての分け方を列挙すると \(2^N - 2\) 通りあり、\(N \leq 200\) では到底間に合いません。
DPによる解決
部分和問題として、「どの合計値が達成可能か」をDPで管理します。ただし、「1個以上 \(N-1\) 個以下を選ぶ」という制約があるため、各合計値に対して選んだ個数の最小値と最大値を記録します。ある合計値 \(s\) について、最小個数が \(N-1\) 以下かつ最大個数が \(1\) 以上であれば、有効な分け方が存在します。
アルゴリズム
- 全体の合計 \(S = \sum A_i\) を計算する。
- DPテーブルとして辞書
dpを用意し、dp[s] = (min_count, max_count)で「合計 \(s\) を達成するのに必要な最小個数と最大個数」を管理する。初期状態はdp[0] = (0, 0)。 - 各お菓子 \(a\) について、「取る」「取らない」の遷移を行い、DPを更新する:
- 取らない場合:合計・個数は変化しない
- 取る場合:合計が \(s + a\) に、個数が \(+1\) になる
- 同じ合計値に複数の個数が対応する場合、最小値と最大値のみ保持する
- 全てのお菓子を処理した後、各達成可能な合計値 \(s\) について、個数が \([1, N-1]\) の範囲に入るものがあるか確認し、\(|2s - S|\) の最小値を求める。
具体例
\(N = 3\), \(A = [1, 2, 3]\) の場合、\(S = 6\)。
- 部分和 \(1\)(1個)→ \(|2 \cdot 1 - 6| = 4\)
- 部分和 \(2\)(1個)→ \(|2 \cdot 2 - 6| = 2\)
- 部分和 \(3\)(1個 or 2個)→ \(|2 \cdot 3 - 6| = 0\) ← 最小!
- 部分和 \(4\)(2個)→ \(|2 \cdot 4 - 6| = 2\)
- 部分和 \(5\)(2個)→ \(|2 \cdot 5 - 6| = 4\)
答えは \(0\)(例えば高橋君に \(\{3\}\)、青木君に \(\{1, 2\}\))。
計算量
- 時間計算量: \(O(N \cdot S)\)(\(S = \sum A_i \leq 200000\))
- 各お菓子について、DPテーブルの全エントリを走査・更新する
- 空間計算量: \(O(S)\)
- DPテーブルのエントリ数は最大 \(S+1\) 個
実装のポイント
各合計値に対して選んだ個数の最小値・最大値のみ保持することで、メモリと計算量を抑えている。個数が連続的に達成可能でなくても、min/maxの範囲が \([1, N-1]\) と重なるかの判定で十分。
DPの更新時に「取る」「取らない」を同時に新しい辞書に書き込むことで、同じお菓子を重複して使うことを防いでいる。
\(N \geq 2\) であるため、片方が全部取る場合(個数 \(0\) または \(N\))を除外する必要があるが、min/max による判定で自然に処理できる。
ソースコード
def solve():
N = int(input())
A = list(map(int, input().split()))
S = sum(A)
# DP: dp[s] = True if subset sum s is achievable
# We need subsets of size 1 to N-1
# dp[j] = set of possible subset sizes that achieve sum j, but simpler:
# Track (sum, count) pairs
# Since N<=100 and S<=100000, use dp[s] as a set of possible counts, or
# just track dp[s] = bitmask of possible counts... N<=100 so bitmask too large
# Alternative: dp[s] = (min_count, max_count) of items that can form sum s
# We need count >= 1 and count <= N-1
# If any achievable count for sum s is in [1, N-1], it's valid
# Better: dp[s] stores min and max number of items to achieve sum s
# If min_count <= N-1 and max_count >= 1, then valid
# dp as dict: sum -> (min_items, max_items)
# Initialize: dp[0] = (0, 0)
dp = {0: (0, 0)}
for a in A:
new_dp = {}
for s, (mn, mx) in dp.items():
# Don't take this item
if s not in new_dp:
new_dp[s] = (mn, mx)
else:
old_mn, old_mx = new_dp[s]
new_dp[s] = (min(old_mn, mn), max(old_mx, mx))
# Take this item
ns = s + a
nmn, nmx = mn + 1, mx + 1
if ns not in new_dp:
new_dp[ns] = (nmn, nmx)
else:
old_mn, old_mx = new_dp[ns]
new_dp[ns] = (min(old_mn, nmn), max(old_mx, nmx))
dp = new_dp
ans = float('inf')
for s, (mn, mx) in dp.items():
# Check if there's a valid count (between 1 and N-1)
if mx >= 1 and mn <= N - 1:
diff = abs(S - 2 * s)
ans = min(ans, diff)
print(ans)
solve()
この解説は claude4.6opus-thinking によって生成されました。