Day 171-Q2 — 2026-10-06 | 箱の塔(交換議論で積む順を s+w に固定し、重さのナップサック DP に落とす)

2026-10-06 赤色 Master (Phase 8+) ★★★★★★★★★ 高度 DP(焦点帯 M1)。「部分集合を選ぶ」と「並べる順」が両方ある問題は、そのままでは $N!$ 通りの順が残る。隣り合う 2 つを入れ替えても悪くならない順(交換議論)を先に決めると、順は 1 通りになり、残りは普通のナップサックになる

問題

箱が $N$ 個ある。箱 $i$ は重さ $w_i$、丈夫さ $s_i$、価値 $v_i$ を持つ。

いくつかの箱を選び、好きな順に縦一列に積んで塔を作る。次の条件をすべて満たす塔の、箱の価値の和の最大値を求めよ(箱を 1 つも選ばない塔も認め、その価値は $0$)。

  • 塔の各箱について、その箱より上にある箱の重さの和がその箱の丈夫さ $s_i$ 以下
  • 塔全体の重さの和が床の耐荷重 $C$ 以下

入力形式

N C
w_1 s_1 v_1
w_2 s_2 v_2
...
w_N s_N v_N

制約

#条件
1$1\le N\le1000$
2$1\le C\le2\times10^4$
3$1\le w_i\le10^4$
4$0\le s_i\le10^4$
5$1\le v_i\le10^9$
6入力はすべて整数

入出力例

入力例1

3 10
2 2 20
1 1 30
3 1 40

出力例1

70

(上から 箱 2、箱 3 と積む。箱 3 の上の重さは 1 で丈夫さ 1 以下。3 つ全部は、どの順でも誰かがつぶれる)

入力例2

4 5
2 10 10
3 0 15
1 1 7
2 2 9

出力例2

26

入力例3

3 9
3 0 11
5 5 1
1 6 15

出力例3

27

(上から 箱 1($s+w=3$)、箱 3($7$)、箱 2($10$)。箱 3 の上は 3 ≤ 6、箱 2 の上は 4 ≤ 5、全体 9 ≤ 9。丈夫さ $s$ の小さい順に積むと箱 2 が箱 3 より上に来て、箱 3 の上が 8 > 6 になり 26 しか取れない。3 例とも全探索で確認)

背景と出典

AtCoder の Educational DP Contest の X 問題「Tower」がこの形(床の耐荷重なし、$N\le1000$、$w,s\le10^4$)だった、と私は記憶している(原典は本セッションでは未確認)。本問は床の耐荷重 $C$ を足した自作の言い換えで、$C$ は DP 配列の長さの上限になるだけ。

交換議論: 隣り合う 2 箱 $a$(上)と $b$(下)を考える。その上の重さを $X$ とすると、条件は $X\le s_a$ かつ $X+w_a\le s_b$。入れ替えると $X\le s_b$ かつ $X+w_b\le s_a$。2 つの条件のうち厳しい方は、それぞれ $X\le\min(s_a,\ s_b-w_a)$ と $X\le\min(s_b,\ s_a-w_b)$ で、

$$s_a+w_a\le s_b+w_b \iff s_b-w_a\ge s_a-w_b$$

なので、$s+w$ が小さい方を上に置けば、上限は入れ替える前より小さくならない。だから上から $s+w$ の小さい順に積む塔だけを考えればよい。

概念図

隣り合う 2 箱 a, b の上に重さ X が載っているとき、許される X の上限を比べる 上の箱たち(重さ X) a(w_a, s_a) b(w_b, s_b) X ≤ s_a かつ X + w_a ≤ s_b 上限 = min(s_a, s_b − w_a) 上の箱たち(重さ X) b(w_b, s_b) a(w_a, s_a) X ≤ s_b かつ X + w_b ≤ s_a 上限 = min(s_b, s_a − w_b) s_a + w_a ≤ s_b + w_b なら s_b − w_a ≥ s_a − w_b → 左(a が上)の上限は  右の上限以上 上から s+w の小さい順に積む 順が 1 通りに決まるので、残りは「いまの塔の重さ t」だけを状態にする 0/1 ナップサック。箱 i を一番下に足せるのは t ≤ s_i のとき

ヒント(クリックで段階的に開示)

ヒント1(方向性)

選んだ箱の集合が決まれば、積む順は 1 通りに決めてよいのではないか。隣り合う 2 箱を入れ替えたとき、上に載せられる重さの上限がどう変わるかを書き出す。

ヒント2(アプローチ)

$s+w$ の小さい順にソートして、上から順に「一番下に足す」形で DP する。状態は「いまの塔の重さ $t$」。箱 $i$ を一番下に足せるのは $t\le s_i$ のとき。

ヒント3(誘導)

dp[t] = 重さ $t$ の塔の価値の最大。箱 $i$ について $t=\min(s_i, C-w_i)$ から $0$ まで下りながら dp[t+w_i] = max(dp[t+w_i], dp[t]+v_i)。答えは max(dp)。更新は合計で最大 $10^7$ 回程度なので、Python でも素直な for 文で間に合う。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n, cap = int(data[0]), int(data[1])
    blocks = []
    for i in range(n):
        w, s, v = int(data[2 + 3 * i]), int(data[3 + 3 * i]), int(data[4 + 3 * i])
        blocks.append((s + w, w, s, v))
    blocks.sort()                            # s+w の小さい順 = 上から順に積む
    NEG = -(1 << 62)                         # 到達不能。v を何度足しても負のまま(v の総和 ≤ 10^12)
    dp = [0] + [NEG] * cap                   # dp[t]: 上から積んだ塔の総重量が t のときの価値の最大
    for _, w, s, v in blocks:
        if w > cap:
            continue
        hi = min(s, cap - w)                 # いまの重さ t ≤ s のときだけ、この箱を一番下に足せる
        if hi < 0:
            continue
        # 右辺のスライスは代入前に複製されるので、同じ箱を 2 回使うことはない(0/1 ナップサック)
        dp[w:w + hi + 1] = map(max, dp[w:w + hi + 1], map(v.__add__, dp[:hi + 1]))
    print(max(dp))


main()

計算量: ソート $O(N\log N)$、DP は箱ごとに $\min(s_i,C-w_i)+1$ 個の更新で $O(N\cdot\min(\max s, C))$、最大 $10^7$ 回。$N=1000$、$C=2\times10^4$(更新の合計 $5.0\times10^6$〜$7.5\times10^6$ 回)で 0.36〜0.53 s(Python 3.14.7、M1 Mac、負荷平均 5〜6 のときの実測)。

Step-by-Step 解説

1積む順を決める(交換議論)

背景の式の通り、$s_a+w_a\le s_b+w_b$ なら $a$ を上にしても悪くならない。隣り合う逆順の対を 1 つずつ入れ替えていけば、どんな実行可能な塔も「$s+w$ の小さい順」の塔に直せる。だから集合を決めた後の並べ方は考えなくてよい。

2上から積む DP にする

上から順に「いまの塔の一番下に新しい箱を足す」と考えると、足せる条件は「いまの塔の重さ $t\le s_i$」だけで、足した後の重さは $t+w_i$。状態は重さ $t$ だけで閉じる。下から積むと「上に何が来るか」がまだ分からないので、状態が閉じない。

30/1 ナップサックとして更新する

各箱は 1 回しか使えない。dp[w:w+hi+1] = map(max, 旧の dp[w:…], v + 旧の dp[:hi+1]) は、右辺のスライスが代入前に複製されるので、同じ箱の更新が同じ箱の更新に連鎖しない。for 文で書く場合は $t$ を大きい方から回す。

4床の耐荷重

塔全体の重さは最後の $t+w_i$ なので、$t\le C-w_i$ を上限に加えるだけでよい。dp の長さを $C+1$ にしておけば、答えは max(dp)。

迷った他案と採らなかった理由

案計算量採否理由
部分集合 × 全順列$O(2^N\cdot N!)$検算用のみ$N\le7$ のランダム 500 件の正解に使用
bit DP(使った集合 × 重さ)$O(2^N\cdot N)$不採用$N=1000$ では不可能
$s+w$ 順のナップサック、for 文を $t$ の大きい順に$O(N\cdot S)$どれでもよい同じ 3 入力で 0.32〜0.50 s(実測)。読みやすさはこちらが上
同じ DP、内包表記で候補を作って zip で比べる$O(N\cdot S)$どれでもよい0.33〜0.48 s(実測)
同じ DP、スライス+map(max)(本解)$O(N\cdot S)$採用0.36〜0.53 s(実測)。3 通りの差は 1 割以内で、Python 3.14 では書き方より更新回数が効く。0/1 の取り違えが起きにくい形として採った
価値を軸にした DP(価値ごとの最小重さ)$O(N\cdot\sum v)$不採用$v\le10^9$ では配列が作れない

前提が変わったら:

  • $s,w$ が $10^9$、$v$ が小さいなら、軸を入れ替えて「価値 $V$ を達成する塔の最小重さ」を DP する(箱を足せる条件は「最小重さ $\le s_i$」で、同じ順で成り立つ)
  • 箱の個数制限つき(同じ箱が $k_i$ 個)なら、同じ箱どうしは $s+w$ が同じなので連続して積まれる。2 冪分割で $2^j$ 個の束にするとき、束の一番下の箱には束の中の $(2^j-1)w$ が載るので、束の丈夫さは $s-(2^j-1)w$、重さは $2^jw$ にする。すると束の $s'+w'=s+w$ で順は変わらない(導出。本セッションでは未実装・未検証)
  • 上に載る「個数」の制限(重さでなく箱の数)なら、比較は $s_i+1$ になり、順は $s$ の小さい順でよい

よくあるミス

ミス原因正しい書き方
丈夫さ $s$ の小さい順に積む「弱い箱を上に」で十分だと思う$s+w$ の小さい順(500 件中 53 件誤答、実測。入力例 3 が反例)
重さ $w$ の小さい順に積む「軽い箱を上に」で十分だと思う500 件中 63 件誤答(実測)
$s-w$ の小さい順に積む交換議論の式の符号を誤る500 件中 165 件誤答(実測)
下から積む DP にする上に載る重さがまだ決まっていない上から積み、一番下に足す形にする
更新を $t$ の小さい順に for 文で回す0/1 と個数無制限の取り違え大きい順に回すか、旧い配列のスライスから計算する

次のステップ

  • 発展1: $w,s\le10^9$、$v\le100$、$N\le100$ の版を、価値を軸にした DP で解き、全探索で確かめる
  • 発展2: 交換議論が効く他の問題(締め切り付きの仕事の並べ替え、Johnson の 2 機械フローショップ)で、比較の式を自分で導く
  • 発展3: 原典の EDPC X「Tower」の制約を確かめ、本解で通るか試す(本セッションでは未確認)

検証ログ(本セッションで実行した数値)

  • 全探索との照合(実測): $N\le7$、$w,s$ の上限を 5/10/30 に振り、$C$ を $1$ から(上限 $\times N$)まで選んだランダム 500 件で不一致 0。全探索は部分集合ごとに全順列を試す
  • 誤った順(実測): 同じ 500 件で、$s$ の順 53 件・$w$ の順 63 件・$s-w$ の順 165 件誤答
  • 入力例(実測): 例 1 70、例 2 26、例 3 27(全探索と一致。例 3 は $s$ の順だと 26、$w$ の順だと 16)
  • 実行時間(実測、負荷平均 5〜6): $N=1000$、$C=2\times10^4$ で($w$ 1〜$10^4$、$s$ 0〜$10^4$)0.37 s、($w$ 1〜10)0.38 s、($w$ 1〜100、$s$ 5000〜$10^4$)0.53 s。$N=500$ では 0.21〜0.36 s。同じ入力を負荷平均 36 のときに測ると 2.2〜2.9 s で、数字は機械の混み具合で 5 倍以上ぶれる
  • 扱っていない範囲: 価値を軸にした DP、個数制限つきの箱、原典の確認

自己評価

自分の回答

気づき・メモ