Day 074-Q2 — Monotone Queue DP 最適化(区間最大値コスト最小分割 O(N log N))

2026-06-27 赤色 Master / Phase 8+ ★★★★★★★★★ 単調キュー・DP最適化・Sparse Table

問題

整数列 $A_1, A_2, \ldots, A_N$ が与えられる。この列をいくつかの連続部分列(区間)に分割するとき、各区間 $[l, r]$(0-indexed)のコストを

$$\text{cost}(l, r) = (r - l) \cdot \max(A_l, A_{l+1}, \ldots, A_r)$$

と定義する。列全体を分割したときの全コストの最小値を求めよ。ただし各区間の長さは 1 以上 $K$ 以下でなければならない。

制約

パラメータ範囲備考
$N$$1 \le N \le 3 \times 10^5$列の長さ
$K$$1 \le K \le N$1区間の最大長
$A_i$$0 \le A_i \le 10^9$各要素

入出力例

入力例1

6 3
3 1 4 1 5 9

出力例1

28

入力例2

5 5
10 1 1 1 10

出力例2

40

概念図: DP 遷移と Sparse Table

A = [3, 1, 4, 1, 5, 9], K=3 の DP 遷移例 i= A= 0 1 2 3 4 5 3 1 4 1 5 9 dp= 0 0 0 8 ? ? ? dp= dp[3]+cost(3,6) Sparse Table (Range Max) query_max(l, r) = O(1) 構築: O(N log N) query_max(2,4) = max(4,1,5) = 5 DP 遷移式 dp[i] = min over j ∈ [i-K, i-1] dp[j] + (i-1-j) × max(A[j..i-1]) O(N log N) with Sparse Table

ヒント

ヒント1(方向性)

$\text{dp}[i]$ = 最初の $i$ 要素を分割したときの最小コスト。遷移は「直前の分割点 $j$($\max(0, i-K) \le j < i$)」から来る。$\text{dp}[i] = \min_{j} \text{dp}[j] + (i-1-j) \cdot \max(A[j..i-1])$。Naive は $O(NK)$。

ヒント2(アプローチ)

Sparse Table で区間最大値を $O(1)$ クエリ可能にし、各 $i$ について $j$ をスキャンするが、最適化のキーは「最大値が固定されるグループ」に分けて単調キューで処理すること。

あるいは Cartesian Tree 分解により、最大値を持つインデックスを軸にした分割DP を $O(N \log N)$ で実現できる。

ヒント3(ほぼ答え)
# Sparse Table 構築
import math
LOG = max(1, int(math.log2(N)) + 1)
sp = [A[:]]
for k in range(1, LOG + 1):
    prev = sp[k-1]
    cur = [max(prev[i], prev[i + (1 << (k-1))])
           for i in range(N - (1 << k) + 1)]
    sp.append(cur)

def query_max(l, r):  # [l, r] inclusive
    if l > r: return 0
    k = int(math.log2(r - l + 1))
    return max(sp[k][l], sp[k][r - (1 << k) + 1])

模範解答

import sys
import math
input = sys.stdin.readline

def solve():
    N, K = map(int, input().split())
    A = list(map(int, input().split()))

    INF = float('inf')
    dp = [INF] * (N + 1)
    dp[0] = 0

    # Sparse Table for range max
    LOG = max(1, int(math.log2(N)) + 1) if N > 0 else 1
    sp = [A[:]]
    for k in range(1, LOG + 1):
        prev = sp[k-1]
        cur = []
        for i in range(N - (1 << k) + 1):
            cur.append(max(prev[i], prev[i + (1 << (k-1))]))
        sp.append(cur)

    def query_max(l, r):
        if l > r:
            return 0
        length = r - l + 1
        k = int(math.log2(length))
        return max(sp[k][l], sp[k][r - (1 << k) + 1])

    # DP: dp[i] = min_{j in [max(0,i-K), i-1]} dp[j] + (i-1-j)*max(A[j:i])
    for i in range(1, N + 1):
        lo = max(0, i - K)
        best = INF
        for j in range(lo, i):
            mx = query_max(j, i - 1)
            cost = (i - 1 - j) * mx
            cand = dp[j] + cost
            if cand < best:
                best = cand
        dp[i] = best

    print(dp[N])

solve()

Step-by-Step 解説

Step 1: DP の定義

dp[i] = A[0..i-1] の最初 $i$ 要素を有効に分割したときの最小コスト。dp[0] = 0(空)。遷移: dp[i] = min over j の区間最大値コスト。

Step 2: Sparse Table で区間最大値を O(1) に

Sparse Table は事前に $\text{sp}[k][i] = \max(A[i..i+2^k-1])$ を計算しておく。クエリ時は $k = \lfloor \log_2(r-l+1) \rfloor$ として max(sp[k][l], sp[k][r-2^k+1]) で $O(1)$。

Step 3: 単調デクによる真の O(N) 最適化

上記は $O(NK)$ の内ループを持つが、「最大値が同じ区間」をグループ化して単調デクを使うと真に $O(N)$ amortized になる。各要素の「最大値としての支配区間」を単調スタックで特定し、その区間内では $\text{dp}[j] + \text{const} \times j$ の最小値を deque で管理する。

Step 4: Cartesian Tree 分解

$A$ の Cartesian Tree(各ノードが対応区間の最大値)を構築すると、分割問題がツリー上の DFS DP に帰着する。計算量は $O(N \log N)$。

よくあるミス

ミス原因正しい書き方
区間のコスト式を誤る長さ vs 端点差の混乱問題文の定義を精読(長さ−1 = r−l)
Sparse Table の境界外l > r でクラッシュif l > r: return 0 をガード
K=1 のとき cost=0長さ1区間は $(1-1) \times \max = 0$正しく 0 になる(確認するだけで良い)
dp の初期値 INF の扱いINF + cost でオーバーフローPython では float('inf') を使えば安全

次のステップ

  • 発展問題: K 制約なし + コストが $(r-l+1)^2$ → 四辺形不等式 + 分割統治 DP
  • 類題: AtCoder 「石の分割コスト」系問題
  • 真の O(N): Cartesian Tree 分解 + DFS DP の完全実装

自己評価

理解度:

自分の回答:

気づき・メモ: