問題
数直線上に店が $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(方向性)
店も倉庫も座標順に並べると、店が右に行くほど最良の倉庫も右に行く(左に戻らない)。「同点なら一番左」を最良とすれば、行ごとの最良の列は単調に増える。
ヒント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 段:
- REDUCE: 列を順に見て、スタックの一番上の列 $t$ と新しい列 $c$ を行
rows[len(st)-1]で比べ、$c$ の方が小さければ $t$ を捨てる。スタックの長さが行数未満なら $c$ を積む。結果、列は行数以下になる - 奇数番目の行だけで再帰
- 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 s | 0.66 s |
| $N=M=2\times10^5$、$c=0$ | 240 万 | 374 万 | 0.29 s | 0.46 s |
| $N=M=2\times10^5$、$c\le10^{12}$ | 219 万 | 374 万 | 0.36 s | 0.55 s |
| $N=2\times10^5,M=2000$ | 44 万 | 24 万 | 0.11 s | 0.15 s |
| $N=2000,M=2\times10^5$ | 82 万 | 220 万 | 0.13 s | 0.32 s |
| $N=M=10^6$、$c\le10^{18}$ | 603 万 | 2094 万 | 1.68 s | 4.61 s |
| $N=M=10^6$、$c=0$ | 1200 万 | 2095 万 | 2.12 s | 2.55 s |
| $N=M=10^6$、$c\le10^{12}$ | 1016 万 | 2094 万 | 2.34 s | 2.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 法、最悪の評価回数になる入力の構成