Day 143-Q2 — 分割統治法によるDP高速化(D&C Optimization)

2026-09-04 赤色 Master / Phase 8+ ★★★★★★★★★ opt(i)の単調性を利用しO(N²)をO(N log N)へ削減

問題

$N$個の整数列 $a_1,\dots,a_N$ を、ちょうど$K$個の空でない連続区間に分割する。各グループのコストを「含まれる要素の総和の2乗」とし、全グループのコスト総和を最小化せよ。

$\mathrm{prefix}(i)=a_1+\dots+a_i$として、区間$(j,i]$のコストは$\mathrm{cost}(j,i)=(\mathrm{prefix}(i)-\mathrm{prefix}(j))^2$。この関数は四角不等式を満たすため、$dp[k][i]=\min_{j

入力形式

N K
a_1 a_2 ... a_N

制約

$1 \le K \le N \le 2\times10^5$
$0 \le a_i \le 10^4$

入出力例

入力例1

5 2
1 2 3 4 5

出力例1

117

{1,2,3}と{4,5}に分割:6²+9²=36+81=117 が最小。

概念図:探索範囲の絞り込み

opt(i)の単調性による探索範囲の絞り込み 区間[lo,hi]=[1,5], 探索範囲[opt_lo,opt_hi]=[0,4] mid=3 の最適j を [0,4] 全体から探す → best_j=2 左: [lo,mid-1]=[1,2], 範囲は[0, best_j=2]に縮小 右: [mid+1,hi]=[4,5], 範囲は[best_j=2, 4]に縮小 各段で探索範囲の合計はO(N)、深さO(log N) ⇒ 各層 O(N log N) K層繰り返すため全体 O(NK log N)

ヒント(段階的開示)

ヒント1(方向性)

素朴な$dp[k][i]=\min_{j

ヒント2(アプローチ)

区間$[lo,hi]$の中央$mid$の最適$j$を$[optlo,opthi]$の範囲だけ探索して求め、その結果を軸に左半分の探索範囲は$[optlo,\mathrm{opt}(mid)]$、右半分は$[\mathrm{opt}(mid),opthi]$に絞る。各$j$が調べられる回数はならし$O(\log N)$回に抑えられる。

ヒント3(誘導)
def solve_layer(dp_prev, N, k, cost):
    dp_cur = [float('inf')] * (N + 1)

    def rec(lo, hi, opt_lo, opt_hi):
        if lo > hi:
            return
        mid = (lo + hi) // 2
        best_val, best_j = float('inf'), opt_lo
        for j in range(opt_lo, min(mid - 1, opt_hi) + 1):
            if dp_prev[j] == float('inf'):
                continue
            v = dp_prev[j] + cost(j, mid)
            if v < best_val:
                best_val, best_j = v, j
        dp_cur[mid] = best_val
        rec(lo, mid - 1, opt_lo, best_j)
        rec(mid + 1, hi, best_j, opt_hi)

    rec(k, N, k - 1, N - 1)
    return dp_cur

模範解答 (Python)

import sys

def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    K = int(data[idx]); idx += 1
    a = [int(x) for x in data[idx:idx + N]]; idx += N

    prefix = [0] * (N + 1)
    for i in range(N):
        prefix[i + 1] = prefix[i] + a[i]

    def cost(j, i):
        s = prefix[i] - prefix[j]
        return s * s

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

    for k in range(1, K + 1):
        dp_cur = [INF] * (N + 1)

        def rec(lo, hi, opt_lo, opt_hi):
            if lo > hi:
                return
            mid = (lo + hi) // 2
            best_val, best_j = INF, opt_lo
            hi_j = min(mid - 1, opt_hi)
            for j in range(opt_lo, hi_j + 1):
                if dp_prev[j] == INF:
                    continue
                v = dp_prev[j] + cost(j, mid)
                if v < best_val:
                    best_val, best_j = v, j
            dp_cur[mid] = best_val
            rec(lo, mid - 1, opt_lo, best_j)
            rec(mid + 1, hi, best_j, opt_hi)

        rec(k, N, k - 1, N - 1)
        dp_prev = dp_cur

    print(dp_prev[N])

main()

Step-by-Step 解説

1DPの定式化
$dp[k][i]$=最初の$i$要素をちょうど$k$グループに分割したときの最小コスト総和。素朴には各層$O(N^2)$。
2四角不等式と単調性
$\mathrm{cost}(j,i)$は四角不等式を満たすため最適$j=\mathrm{opt}(i)$は$i$の増加に対して単調非減少になる。
3分割統治による絞り込み
中央の最適$j$を軸に左右の探索範囲を分割し、各層の探索総量をならし$O(N\log N)$に抑える。
4層をまたぐ計算量
K層それぞれ$O(N\log N)$のため全体$O(NK\log N)$。
5具体例のトレース
$N=5,K=2$で$k=1$層は$dp[1][i]=\mathrm{prefix}(i)^2$、$k=2$層で$dp[2][5]=117$(j=3、{1,2,3},{4,5})が最小。

よくあるミス

ミス原因正しい書き方
探索範囲の上限をmin(mid,opt_hi)にしてしまうj=midを許すと右側グループが空になるmin(mid-1,opt_hi)としてj<midを保証
コスト関数が四角不等式を満たすか確認せず適用単調性が成立しない一般関数では最適解を見落とす適用前にコスト関数の凸性・四角不等式を確認
dp_prev[j]がINF(未到達)のケースを無視kグループには最低k要素必要で範囲外jは無効dp_prev[j]==INFのときはスキップ
再帰の基準区間をkに依存しない形で渡すkグループではi≥k, j≥k-1でなければ無意味rec(k, N, k-1, N-1)のように層ごとの有効範囲を渡す
再帰関数をループ内で毎回defしオーバーヘッド増関数呼び出しコストを軽視大規模データでは非再帰化やPyPy/C++移植を検討

次のステップ

  • 発展: コスト関数の凸性が保証されない区間DP向けのKnuthの最適化との違いを整理せよ。
  • 発展2: Kが大きい場合に有効なAliens Trick (WQS binary search)で$O(N\log N\log(\text{value range}))$に落とす手法と比較せよ。

自己評価

自分の回答

気づき・メモ