Day 180-Q3 — 2026-10-16 | SMAWK で Monge 行列の行ごとの最小値(費用 c_j+|x_i−y_j|³ の倉庫選び。分割統治(monotone minima)との評価回数・時間の比較つき)

2026-10-16 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。LARSCH 法(1 次元 DP の $O(N)$ 解法)の中身は SMAWK 法なので、まず「行列が最初から全部決まっている」場合の SMAWK を書いて、評価回数が本当に $O(N+M)$ になるかと、Python で速くなるかを測る。費用が $|x-y|^3$ と固定費の和なので、直線の最小(CHT)にも「近い倉庫だけ見る」にもならない

問題

数直線上に店が $N$ 軒(座標 $x_1,\dots,x_N$)、倉庫が $M$ 棟(座標 $y_1,\dots,y_M$、固定費 $c_1,\dots,c_M$)ある。店 $i$ が倉庫 $j$ から仕入れる費用は

$$A_{ij}=c_j+|x_i-y_j|^3$$

である。各店 $i$ について $\min_jA_{ij}$ を求めよ(店ごとに独立に選んでよい)。

入力形式

N M
x_1 ... x_N
y_1 ... y_M
c_1 ... c_M

制約

#制約
1$1\le N,M\le10^6$
2$0\le x_i,y_j\le10^6$
3$0\le c_j\le10^{18}$
4出力は入力の店の順に 1 行ずつ

入出力例

入力例1

4 4
3 11 0 2
5 2 4 3
5 50 100 100

出力例1

13
221
58
32

(座標 2 の店は、真上の倉庫(座標 2、固定費 50)

入力例2

2 1
1000000 0
0
1000000000000000000

出力例2

2000000000000000000
1000000000000000000

($10^{18}+(10^6)^3=2\times10^{18}$。64 ビット符号付き整数の上限 $\approx9.2\times10^{18}$ には収まる)

背景と出典

  • SMAWK 法は Aggarwal, Klawe, Moran, Shor, Wilber「Geometric applications of a matrix-searching algorithm」(1987) と記憶している(原典は未確認)。名前は著者の頭文字
  • 分割統治で行の最小を求める方法(monotone minima)は競技プログラミングでよく使われる $O((N+M)\log N)$ の方法。LARSCH 法(Larmore–Schieber 1991 と記憶、未確認)は SMAWK を 1 次元 DP の「行が前の答えに依存する」場合に使えるようにしたもの
  • $f$ が凸なら $f(x_i-y_j)$ は $x,y$ を整列すると Monge になる、は標準的な事実(証明は Step 1 に書いた)
  • 本問の問題文・制約・入力例は自作

概念図

入力例 1 を座標順に並べた行列 A[i][j] = c_j + |x_i − y_j|³(緑 = 行の最左最小) y=2, c=50 y=3, c=100 y=4, c=100 y=5, c=5 x=0 58 127 164 130 x=2 50 101 108 32 x=3 51 100 101 13 x=11 779 612 443 221 最左最小の列は 0, 3, 3, 3 と右へしか動かない(Monge) x = 2 の行:真上の倉庫(50)より遠い安い倉庫(32) → 最寄りだけ・ポインタを進めるだけは 50 と誤る SMAWK の 1 段 REDUCE:どの行の最小にもならない列を捨て、列 ≤ 行 奇数番目の行だけで再帰 INTERPOLATE:偶数行は上下の答えの間だけ見る 評価回数 O(N + M)(分割統治は O((N + M) log N))

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

ヒント1(方向性)

店も倉庫も座標順に並べると、店が右に行くほど最良の倉庫も右に行く(左に戻らない)。「同点なら一番左」を最良とすれば、行ごとの最良の列は単調に増える。

ヒント2(アプローチ)

単調性だけなら、真ん中の行の最良を全列から探し、上半分はそれより左・下半分はそれより右だけを探す分割統治で $O((N+M)\log N)$。さらに Monge 性($A_{ij}+A_{i+1,j+1}\le A_{i,j+1}+A_{i+1,j}$)を使うと、どの行の最良にもならない列を先に捨てられて $O(N+M)$ になる(SMAWK)。

ヒント3(誘導)

SMAWK の 1 段:

  1. REDUCE: 列を順に見て、スタックの一番上の列 $t$ と新しい列 $c$ を行 rows[len(st)-1] で比べ、$c$ の方が小さければ $t$ を捨てる。スタックの長さが行数未満なら $c$ を積む。結果、列は行数以下になる
  2. 奇数番目の行だけで再帰
  3. INTERPOLATE: 偶数番目の行の最良は、上下の奇数番目の行の最良の間にある。列を左から 1 回なめるだけで全部決まる

模範解答 (Python)

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


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    xs = list(map(int, data[2:2 + n]))
    ys = list(map(int, data[2 + n:2 + n + m]))
    cs = list(map(int, data[2 + n + m:2 + n + 2 * m]))
    ro = sorted(range(n), key=xs.__getitem__)          # 店を座標順に
    co = sorted(range(m), key=ys.__getitem__)          # 倉庫を座標順に
    X = [xs[i] for i in ro]
    Y = [ys[j] for j in co]
    Cc = [cs[j] for j in co]
    ans = [0] * n                                      # 座標順の行 i の最左最小の列

    def smawk(rows, cols):
        nr = len(rows)
        if nr == 0:
            return
        # REDUCE:どの行の最左最小にもならない列を捨て、列数を行数以下にする
        st = []
        for c in cols:
            yc, cc = Y[c], Cc[c]
            while st:
                x = X[rows[len(st) - 1]]
                t = st[-1]
                d1 = x - Y[t]
                d2 = x - yc
                if d1 < 0: d1 = -d1
                if d2 < 0: d2 = -d2
                if Cc[t] + d1 * d1 * d1 <= cc + d2 * d2 * d2:
                    break
                st.pop()
            if len(st) < nr:
                st.append(c)
        # 奇数番目の行だけで再帰
        smawk(rows[1::2], st)
        # INTERPOLATE:偶数番目の行は、上下の行の答えの間だけを見る
        k = 0
        last = st[-1]
        for t in range(0, nr, 2):
            r = rows[t]
            x = X[r]
            hi = ans[rows[t + 1]] if t + 1 < nr else last
            bc = st[k]
            d = x - Y[bc]
            if d < 0: d = -d
            best = Cc[bc] + d * d * d
            while st[k] != hi:
                k += 1
                c = st[k]
                d = x - Y[c]
                if d < 0: d = -d
                v = Cc[c] + d * d * d
                if v < best:
                    best, bc = v, c
            ans[r] = bc

    smawk(list(range(n)), list(range(m)))
    res = [0] * n
    for i in range(n):
        d = X[i] - Y[ans[i]]
        if d < 0: d = -d
        res[ro[i]] = Cc[ans[i]] + d * d * d
    sys.stdout.write("\n".join(map(str, res)) + "\n")


main()

計算量: 並べ替え $O(N\log N+M\log M)$、SMAWK 本体は行列の要素の評価が $O(N+M)$ 回。再帰の深さは $\log_2N$。実測(Python 3.14、Apple M5 Pro、起動込み)で $N=M=2\times10^5$ が 0.11〜0.36 s、$N=M=10^6$ が 1.68〜2.34 s・最大 622 MB。

Step-by-Step 解説

1Monge であることの確認

$x_i\le x_{i+1}$、$y_j\le y_{j+1}$ とし、$f(d)=|d|^3$(凸関数)とする。4 つの引数 $x_i-y_{j+1}\le\{x_i-y_j,\ x_{i+1}-y_{j+1}\}\le x_{i+1}-y_j$ は、真ん中 2 つの和と外側 2 つの和が等しい。凸関数では「同じ和なら、外側に開いた方が値の和が大きい」ので

$$f(x_i-y_j)+f(x_{i+1}-y_{j+1})\le f(x_i-y_{j+1})+f(x_{i+1}-y_j)$$

固定費 $c_j$ は両辺に同じだけ足されるので、$A$ は Monge。Monge なら「行の最左最小の列」は行とともに増える(増えないと仮定すると不等式に矛盾する)。

2なぜ簡単な方法では駄目か

  • 直線の最小(CHT): $|x-y|^3$ は $x$ の 1 次式でないので使えない
  • 最寄りの倉庫だけ: 固定費があるので、遠くの安い倉庫が勝つ(入力例 1)。600 入力中 319 で誤り(実測)
  • 前の店の答えから「次の方が安い間だけ進む」: 行の中で費用は単峰でない(固定費がばらばら)ので、途中の高い倉庫で止まる。600 入力中 282 で誤り(実測)

3REDUCE が捨ててよい理由

スタックの長さを $h$ とし、一番上の列 $t$ と新しい列 $c$($t<c$)を行 $r_{h-1}$ で比べて $A_{r_{h-1},c}<A_{r_{h-1},t}$ なら、Monge 性から行 $r_{h-1}$ 以降のすべての行でも $c$ が $t$ より小さい。行 $r_0..r_{h-2}$ では、スタックの中の $t$ より前の列が $t$ に勝つことが積んだ時点の比較で分かっている。だから $t$ はどの行の最左最小にもならない。スタックの長さが行数に達したら、新しい列は積まない(最後の行より下の行は無いので、その列が最小になる行は無い)。

4INTERPOLATE

奇数番目の行の答えが再帰で決まると、偶数番目の行 $r_{2k}$ の答えは $\mathrm{ans}(r_{2k-1})$ と $\mathrm{ans}(r_{2k+1})$ の間にある。列のポインタ k を戻さずに進めるので、1 段の手間は「行数+列数」。REDUCE で列が行数以下になっているから、各段の手間は行数に比例し、行数が半分ずつ減るので全体 $O(N+M)$。

5速さの工夫

最初は費用を関数 f(i, j) にしていたが、$N=M=10^6$・固定費 0 で 3.24 s と、分割統治(2.55 s)より遅かった(評価回数は 1200 万回対 2095 万回で SMAWK の方が少ないのに)。Python では関数呼び出し 1 回の重さが評価 1 回より大きいので、費用の式をループの中に直接書いて 2.12 s にした。

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

案評価回数($N=M=10^6$、固定費 $\le10^{12}$)時間採否理由
全部見る$NM=10^{12}$—検算用のみ小さい入力の全探索に使った
分割統治(monotone minima)2094 万回2.85 s不採用正しい。単純で、行が少ない形($M\ll N$)では SMAWK より評価が少ない(下表)
最寄りの倉庫だけ$O(N)$—不採用(誤り)1200 入力中 647 で誤り(実測)
前の答えから進むポインタ$O(N+M)$—不採用(誤り)1200 入力中 557 で誤り(実測)
SMAWK(本解)1016 万回2.34 s採用評価回数は分割統治の約半分

評価回数と時間の比較(実測、起動込み。評価回数は費用を計算した回数):

入力SMAWK 評価分割統治 評価SMAWK 時間分割統治 時間
$N=M=2\times10^5$、$c\le10^{18}$121 万372 万0.26 s0.66 s
$N=M=2\times10^5$、$c=0$240 万374 万0.29 s0.46 s
$N=M=2\times10^5$、$c\le10^{12}$219 万374 万0.36 s0.55 s
$N=2\times10^5,M=2000$44 万24 万0.11 s0.15 s
$N=2000,M=2\times10^5$82 万220 万0.13 s0.32 s
$N=M=10^6$、$c\le10^{18}$603 万2094 万1.68 s4.61 s
$N=M=10^6$、$c=0$1200 万2095 万2.12 s2.55 s
$N=M=10^6$、$c\le10^{12}$1016 万2094 万2.34 s2.85 s

(SMAWK の時間は費用の式を直接書いた版。評価回数は関数版で数えた。分割統治は最初から式を直接書いている)

固定費が大きくばらつくと REDUCE で大半の列が捨てられ、SMAWK の評価は分割統治の 3 割程度まで減る。固定費 0 では捨てられる列が少なく、評価は 6 割程度で、時間差も小さい。

前提が変わったら

  • 行が前の行の答えに依存する 1 次元 DP($\mathrm{dp}_j=\min_{i<j}\mathrm{dp}_i+w(i,j)$、$w$ が Monge)なら、行列が先に決まらないので SMAWK はそのまま使えない。LARSCH 法か、$O(N\log N)$ の「単調キュー+二分探索」にする
  • 費用が $x$ の 1 次式+$y$ だけの項なら CHT(直線の最小)で十分で、SMAWK は要らない
  • 列が少ない($M\ll N$)なら、分割統治の方が評価が少ない(上表の $M=2000$)

よくあるミス

ミス原因正しい書き方
座標順に並べ替えない入力順のままで単調性を使う店も倉庫も座標順にして、出力は元の順に戻す
REDUCE で同点のとき捨てる<= と < の取り違え新しい列が真に小さいときだけ上の列を捨てる(最左最小を保つ)
INTERPOLATE で毎行ポインタを 0 に戻す区間の左端を毎回探すポインタを戻さず前の行の答えの位置から進む
最後の偶数行の右端を決め忘れる下の奇数行が無い右端は残った列の最後
費用を関数にして遅くなる評価回数だけを見るPython では式を直接書く(Step 5)

次のステップ

  • 発展1: LARSCH 法を書き、$w(i,j)=(S_j-S_i-L)^2$ 型の 1 次元 DP で、「単調キュー+二分探索」と評価回数・時間を比べる
  • 発展2: 2 次元の Monge 性(Knuth–Yao の四角不等式)を使う区間 DP の $O(N^2)$ 化と、SMAWK による行最小の関係を整理する
  • 発展3: 費用が $|x-y|^{1.5}$ のような非整数の指数のとき、浮動小数の誤差で単調性が崩れる例を探す

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

  • 全探索との一致(実測): $N,M\le40$、座標の範囲 4 通り(5・30・1000・$10^6$)、固定費の範囲 5 通り(0〜$10^{18}$)の乱択 1200 入力(乱数の種 2 つ)で、SMAWK・分割統治とも全探索と全件一致
  • 誤った方法(実測、同じ 1200 入力): 前の答えから進むポインタ 557、最寄りの倉庫だけ 647 で誤り
  • 大きい入力の照合(実測): 上表の 8 入力すべてで SMAWK と分割統治の出力が一致
  • 時間と評価回数: 上表。関数版 SMAWK は $N=M=10^6$ で 2.43 s($c\le10^{18}$)・3.24 s($c=0$)・2.70 s($c\le10^{12}$)
  • 扱っていない範囲: 原典の確認、LARSCH 法、最悪の評価回数になる入力の構成

自己評価

自分の回答

気づき・メモ