Official

E - エレベーターの運搬 / Elevator Transport Editorial by admin Editorial for printing

GPT 5.2 High

概要

\(N \le 15\) 個の荷物を、合計重量が \(C\) 以下になるように何回かに分けて運ぶとき、必要な最小往復回数(グループ数)を求めます。

考察

この問題は本質的に「荷物をいくつかのグループに分け、各グループの合計が \(C\) 以下」という 分割(bin packing) です。一般には難しい問題ですが、今回は \(N \le 15\) と小さいため、全ての部分集合を状態として持つDP が可能です。

素朴に「全ての分け方(グループ分け)を列挙」すると、分割の仕方は爆発的に増え(ベル数のオーダー)、\(N=15\) でも現実的ではありません。また、貪欲法(重い順に詰めるなど)は最適解を保証できず、WAになります。

そこで、「今どの荷物を運び終えたか」を bitmask(部分集合)で管理しつつ、
(往復回数, 現在の便に積んでいる重さ) を最適化するDPを行うと、\(2^N\) 状態で解けます。

ポイントは、最小化したいのは往復回数ですが、同じ往復回数なら「現在便の積載量が小さい」ほうが将来詰めやすく有利、ということです。よって状態の評価を - 往復回数を最小 - 同じなら現在の積載量を最小
という 辞書順(lexicographical) で比較します。

アルゴリズム

状態

\(mask\) を「運び終えた荷物の集合」(\(N\) ビット)とします。

DP配列: - \(dp[mask] = (r, s)\)
- \(r\): これまでに使った往復回数(便の数) - \(s\): 最後の便(現在使っている便)に積んでいる合計重量

ここで \((r, s)\) は辞書順で最小のものを保持します。

初期状態: - \(dp[0] = (1, 0)\)
まだ何も運んでいないが、「これから1便目に積み始める」と考えると遷移が簡単になります。

遷移

各 \(mask\) について、まだ運んでいない荷物 \(i\) を1つ追加することを考えます。

  • もし \(s + W_i \le C\) なら、同じ便に載せられる:
    • \(dp[mask \cup \{i\}]\) の候補は \((r, s + W_i)\)
  • そうでなければ、新しい便を開始する:
    • 候補は \((r + 1, W_i)\)

これを全ての \(mask\) と全ての \(i\) について行い、最小のものを更新します。

例(イメージ)

\(C=10\), 荷物が \([6,5,4]\) のとき、 - まず \(6\) を積むと現在便は \(6\) - 次に \(5\) は \(6+5=11\) で無理なので、便を増やして新便に \(5\) - そこへ \(4\) を積むと \(5+4=9\) でOK
結果、2便で運べます。

この「最後の便の積載量 \(s\) を持っておく」ことで、「あと何が同じ便に入るか」を正しく管理できます。

計算量

  • 時間計算量: \(O(N \cdot 2^N)\)
    各 \(mask\)(\(2^N\) 個)から、最大 \(N\) 個の荷物追加を試すため。
  • 空間計算量: \(O(2^N)\)
    \(dp\) に \(2^N\) 状態を保持するため。

実装のポイント

  • Pythonではタプル同士が辞書順比較できるので、cand < dp[nmask] のように書くだけで
    「往復回数が小さい方、同じなら積載量が小さい方」を自然に選べます。

  • 初期値を dp[0] = (1, 0) とするのがコツです(便数を0から始めると場合分けが増えます)。

  • 最終的な答えは dp[(1<<N)-1][0](全て運び終えた状態の往復回数)です。

    ソースコード

import sys

def main():
    input = sys.stdin.readline
    N, C = map(int, input().split())
    W = list(map(int, input().split()))
    M = 1 << N

    INF = 10**18
    dp = [(INF, INF)] * M
    dp[0] = (1, 0)

    for mask in range(M):
        rides, load = dp[mask]
        for i in range(N):
            if mask & (1 << i):
                continue
            nmask = mask | (1 << i)
            if load + W[i] <= C:
                cand = (rides, load + W[i])
            else:
                cand = (rides + 1, W[i])
            if cand < dp[nmask]:
                dp[nmask] = cand

    print(dp[M - 1][0])

if __name__ == "__main__":
    main()

この解説は gpt-5.2-high によって生成されました。

posted:
last update: