Day 162-Q3 — 2026-09-27 — 凹関数との (max,+) 畳み込みで「重さの種類が少ない」ナップサックを解く

2026-09-27 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。同じ重さの品物をまとめると価値の累積和が凹になり、DP の遷移が「最適な位置が単調に動く」(max,+) 畳み込みになる。$O(NW)$ を $O(D\cdot W\log W)$ に落とす

問題

$N$ 個の品物があり、品物 $i$ の重さは $w_i$、価値は $v_i$。重さの合計が $W$ 以下になるように品物を選ぶ(各品物は高々 1 回)とき、価値の合計の最大値を求めよ。

ただし 重さは小さい: すべての $i$ で $1\le w_i\le 10$。

入力形式

N W
w_1 v_1
w_2 v_2
...
w_N v_N

制約

#条件
1$1\le N\le 2\times10^5$
2$1\le W\le 10^5$
3$1\le w_i\le 10$
4$1\le v_i\le 10^9$
5入力はすべて整数

入出力例

入力例1

5 7
3 10
2 7
2 6
1 3
3 9

出力例1

23

(重さ 3・価値 10、重さ 2・価値 7、重さ 2・価値 6 で重さ 7・価値 23)

入力例2

3 4
3 7
2 4
2 4

出力例2

8

(価値 / 重さ の大きい順に取る貪欲は重さ 3 の品物(比 2.33)を先に取って 7 で止まる。正解は重さ 2 を 2 つで 8)

背景と出典

  • 凹(concave)な数列との (max,+) 畳み込みで最適な添字が単調になる性質は、Monge 行列・totally monotone 行列の探索(SMAWK: Aggarwal・Klawe・Moran・Shor・Wilber 1987)と同じ構造。本問は SMAWK ではなく分割統治(monotone minima)を使う
  • 「重さが小さい品物のナップサック」は理論でも研究されている(例: Polak・Rohwedder・Węgrzycki「Knapsack and Subset Sum with Small Items」ICALP 2021)。本セッションではこれらの論文を参照しておらず、本解の正しさは下の検証ログ(全探索・素朴 DP との照合)だけを根拠にしている

概念図:余りごとの分割と単調な最適位置

① 重さ w=3:遷移は c→c+3 だけ → 余り r ごとに 3 本へ 0 1 2 3 4 5 6 7 8 9 10 11 dp[c](c = 0..11) r=0: dp[0::3] 0 3 6 9 r=1: dp[1::3] 1 4 7 10 r=2: dp[2::3] 2 5 8 11 各列で B[i] = max_j A[j] + P[i−j] P = 価値降順の累積和(凹) P の差分が非増加 ⇒ P[i−j]+P[i′−j′] ≥ P[i−j′]+P[i′−j] ⇒ opt(i) が単調非減少 ② 行 i × 列 j(m=3、有効は i−3 ≤ j ≤ i) 0 1 2 3 4 5 6 7 01234567 ● = opt(i)。右下へ階段状にしか動かない(模式図) 真ん中の行を全探索 → 上は左側、下は右側だけ探す 各深さ O(L)、深さ O(log L) ⇒ O(L log L)

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

ヒント1(方向性)

素朴な 0/1 ナップサックは $O(NW)=2\times10^{10}$。重さは 10 種類しかない。同じ重さの品物の中では、どれを選ぶのが得か はすぐ決まる。

ヒント2(アプローチ)

重さ $w$ の品物を価値の大きい順に並べ、上位 $k$ 個の和を $P_w[k]$ とする。重さ $w$ の品物を $k$ 個選ぶなら上位 $k$ 個が最善。よって

$$\mathrm{dp}'[c]=\max_{k\ge0}\ \mathrm{dp}[c-kw]+P_w[k]$$

重さ $w$ で割った余り $r$ ごとに $A[j]=\mathrm{dp}[r+jw]$ とおくと $B[i]=\max_j A[j]+P_w[i-j]$ という (max,+) 畳み込み。$P_w$ は 凹(増え方が減っていく)。

ヒント3(誘導)

$P$ が凹なら、$B[i]$ を最大にする $j$(の最小のもの)$\mathrm{opt}(i)$ は $i$ について 単調非減少。真ん中の $i$ を全探索で解き、左半分は $j\le\mathrm{opt}(\mathrm{mid})$、右半分は $j\ge\mathrm{opt}(\mathrm{mid})$ に絞って再帰すると $O(L\log L)$。

def solve(lo, hi, jlo, jhi):
    mid = (lo + hi) // 2
    j* = argmax_{j in [max(jlo, mid-m), min(jhi, mid)]} A[j] + P[mid-j]
    B[mid] = ...
    solve(lo, mid-1, jlo, j*); solve(mid+1, hi, j*, jhi)

模範解答 (Python)

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


def maxplus_concave(A, P):
    """B[i] = max_{0<=i-j<len(P)} A[j] + P[i-j]  (P は凹、A は任意)。len(B) = len(A)。
    最適な j は i について単調非減少なので、分割統治で O(L log L)。"""
    L, m = len(A), len(P) - 1
    B = [0] * L
    stack = [(0, L - 1, 0, L - 1)]
    while stack:
        lo, hi, jlo, jhi = stack.pop()
        if lo > hi:
            continue
        mid = (lo + hi) >> 1
        a = mid - m if mid - m > jlo else jlo          # i-j <= m
        b = mid if mid < jhi else jhi                  # j <= i
        best, arg = A[a] + P[mid - a], a
        for j in range(a + 1, b + 1):
            v = A[j] + P[mid - j]
            if v > best:                               # 同点は小さい j を残す(左端の argmax)
                best, arg = v, j
        B[mid] = best
        stack.append((lo, mid - 1, jlo, arg))
        stack.append((mid + 1, hi, arg, jhi))
    return B


def main():
    data = sys.stdin.buffer.read().split()
    n, W = int(data[0]), int(data[1])
    groups = {}
    for i in range(n):
        w, v = int(data[2 + 2 * i]), int(data[3 + 2 * i])
        groups.setdefault(w, []).append(v)
    dp = [0] * (W + 1)                                 # dp[c] = 重さ c 以下での最大価値
    for w, vs in groups.items():
        if w > W:
            continue
        vs.sort(reverse=True)                          # 同じ重さなら価値の大きい順に取るのが最善
        vs = vs[:W // w]                               # 入りきらない分は捨てる
        P = [0]
        for v in vs:
            P.append(P[-1] + v)                        # P[k] = 上位 k 個の和(凹)
        for r in range(w):                             # 重さ w で割った余りごとに独立
            A = dp[r::w]
            dp[r::w] = maxplus_concave(A, P)
    print(dp[W])


main()

計算量: 重さの種類 $D\le10$ ごとに、余り $r$ の列(長さ $\approx W/w$)を $w$ 本処理して合計 $O(W\log W)$。全体 $O(N\log N+D\,W\log W)$。
実測(Python 3.14.7、M1 Mac): $N=2\times10^5$、$W=10^5$ で 1.99 s。$N=2000$、$W=2\times10^4$ では本解 0.29 s、素朴 DP 11.62 s。

Step-by-Step 解説

1同じ重さの中では貪欲でよい

重さが同じ品物を $k$ 個選ぶなら、価値の大きい方から $k$ 個が最善(入れ替えても重さは変わらず価値は減らない)。よって「重さ $w$ を何個使うか」だけが問題で、その価値は $P_w[k]$。
$P_w[k]-P_w[k-1]$ は $k$ 番目に大きい価値なので非増加 → $P_w$ は凹。

2余りごとに分ける

$\mathrm{dp}'[c]$ は $\mathrm{dp}[c],\mathrm{dp}[c-w],\mathrm{dp}[c-2w],\dots$ だけから決まる。$c\bmod w$ が同じ位置だけが互いに関係するので、dp[r::w] を取り出して 1 本の列として畳み込み、dp[r::w] = ... で書き戻す。

3最適な位置が単調になる理由

$M[i][j]=A[j]+P[i-j]$ とおく。$P$ が凹なら $i<i'$、$j<j'$ で

$$P[i-j]+P[i'-j']\ \ge\ P[i-j']+P[i'-j]$$

(差が等しい 2 組では、真ん中寄りの 2 点の和が大きい)。$A$ の項は両辺に同じだけ入るので $M[i][j]+M[i'][j']\ge M[i][j']+M[i'][j]$。もし $\mathrm{opt}(i')<\mathrm{opt}(i)$ なら $j=\mathrm{opt}(i'),\ j'=\mathrm{opt}(i)$ としてこの不等式から矛盾が出る。範囲外($i-j<0$ や $i-j>m$)を $-\infty$ とみなしても凹のまま。

4分割統治(monotone minima)

真ん中の行 $\mathrm{mid}$ の最適な $j$ を範囲内の全探索で求め、上半分は $[j_{lo},\mathrm{opt}]$、下半分は $[\mathrm{opt},j_{hi}]$ に絞る。各深さで探索する $j$ の合計は $O(L)$、深さ $O(\log L)$。再帰の代わりにスタックで回し、Python の再帰上限を避けている。

5凹でなければ壊れる

価値を並べ替えずに累積和を取ると $P$ は凹でなくなり、単調性が崩れる。実測で 500 ケース中 280 ケースが誤答になった。「凹だから速くできる」の「凹」は Step 1 の並べ替えが作っている。

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

案計算量採否理由
素朴な 0/1 ナップサック$O(NW)$検算用のみ$N=2000$、$W=2\times10^4$ で 11.62 s(実測)。本番の $2\times10^{10}$ は不可能
価値 / 重さ の大きい順の貪欲$O(N\log N)$不採用500 ケース中 127 ケースで誤答、最悪で最適値より 27.8% 小さい(実測)。入力例 2 がその形
重さごとに「$k$ 個使う」を全部試す DP$\sum_w W\cdot\min(\#_w, W/w)$不採用各重さの品物が多いと $O(W^2)$ 程度($W=10^5$ で $10^{10}$)になり、素朴 DP と変わらない(見積もり)
SMAWK で (max,+) 畳み込み$O(W)$ 本あたり線形不採用理論は $\log$ 分速いが、Python では定数倍が重く実装も長い。$W=10^5$ で分割統治が 2 s 以内に収まったので十分
近接性(最適解は貪欲解から重さ $O(D^2)$ 程度しか離れない、という型の定理)で DP の範囲を絞る理論上ほぼ線形不採用本セッションでは定理の正確な形を原典で確かめておらず、使える窓幅の根拠を示せない
分割統治 monotone minima(本解)$O(D\,W\log W)$採用全探索 500 ケース・素朴 DP 200 ケースと全件一致

前提が変わったら:

  • 重さの上限 $D$ が大きい(例 $10^3$)と、重さの種類が増えて $D\cdot W\log W$ が重くなる。そのときは種類数の少なさではなく別の構造(価値が小さい→価値で DP、など)を探す
  • 個数制限なし(同じ品物を何度でも)なら重さごとに最大価値の 1 個だけ残せば $P_w[k]=k\cdot v_{\max}$(線形=凹)で、同じ手法がそのまま使える
  • 重さちょうど $W$(以下ではなく)を求めるなら、dp の初期値を $\mathrm{dp}[0]=0$、他を $-\infty$ にする。$-\infty$(実装では $-10^{30}$)を混ぜても本解の分割統治のままで正しく動いた(重さ 2〜10・$W\le80$ のランダム 1000 ケース、うち到達不能 470 ケースで素朴 DP と全件一致、実測)

よくあるミス

ミス原因正しい書き方
価値を並べ替えずに累積和$P$ が凹にならず単調性が崩れる降順に並べてから累積和(並べ替え忘れで 500 中 280 ケース誤答)
分割統治の探索範囲に $j\le i$ と $i-j\le m$ を入れない$P$ の範囲外を読むa = max(jlo, mid-m)、b = min(jhi, mid)
余りごとに分けずに畳み込む重さ $w$ の遷移は $c\to c+w$ だけで、隣の位置とは無関係dp[r::w] で取り出して書き戻す
価値/重さ の貪欲重さの端数が余るDP(貪欲は 127/500 で誤答)

次のステップ

  • 発展1: SMAWK で各列を $O(L)$ にし、Python で分割統治版と実測比較する(Day023・Day119 の SMAWK と接続)
  • 発展2: (min,+) 畳み込みで「凸+凸」は Minkowski 和(差分をマージ)で $O(n)$ になる。本問は「任意+凹」なので $\log$ が付く、という違いを整理する
  • 次回予告: 累積和で遷移を $O(1)$ にする数え上げ DP(転倒数ちょうど $K$ の順列)→ Q4

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

  • 全探索との一致(実測): $N\le12$、$w_i\le6$、$W\le40$、価値の上限を 5 / 100 / $10^9$ から選ぶランダム 500 ケースで、プログラム全体(標準入出力)の出力が部分集合の全列挙と全件一致
  • 素朴 DP との一致(実測): $N\le300$、$w_i\le10$、$W\le1500$、価値の上限 1 / 2 / 3 / 10 / $10^9$(同点が多いケースを含む)のランダム 200 ケースで全件一致
  • 誤りの件数(実測、$N\le30$・$W\le60$ の 500 ケース): 価値を並べ替えない → 280 件、価値/重さ の貪欲 → 127 件(最悪で最適値より 27.8% 小さい)
  • 実行時間(実測): $N=2000,W=2\times10^4$: 本解 0.29 s / 素朴 DP 11.62 s。$N=2\times10^4,W=2\times10^4$: 0.38 s。$N=2\times10^5,W=10^5$: 1.99 s。$N=2\times10^5,W=2\times10^5$: 3.82 s(このため制約を $W\le10^5$ にした)
  • 「重さちょうど $W$」版(実測): 初期値 $-10^{30}$ で 1000 ケース(到達不能 470 ケースを含む)が素朴 DP と全件一致
  • 扱っていない範囲: SMAWK 版の実装と実測、近接性定理による窓の絞り込み、引用論文の原文確認

自己評価

自分の回答

気づき・メモ