説明

$2$ 変数関数 $f(i, j) (0 \leq i \lt H, 0 \leq j \lt W)$ が Monotone であるとは、すべての $k$ に対して $\mathrm{argmin} f(k, *) \leq \mathrm{argmin} f(k + 1, *)$ を満たすことをいう。つまり各行の最小値をとる位置が右下に単調に下がっていることを意味する。

Monge $\Rightarrow$ Totally Monotone(TM) $\Rightarrow$ Monotone なので、Monotone は弱い条件である。

計算量

  • $O((H + W) \log H)$

実装例

  • monotone_minima($H$, $W$, $f$, $comp$): 各行について、最小値をとる位置と最小値をペアで返す。$f$ は $2$ 変数関数。
template< typename T, typename Compare = less< T > >
vector< pair< int, T > > monotone_minima(int H, int W, const function< T(int, int) > &f, const Compare &comp = Compare()) {
  vector< pair< int, T > > dp(H);
  function< void(int, int, int, int) > dfs = [&](int top, int bottom, int left, int right) {
    if(top > bottom) return;
    int line = (top + bottom) / 2;
    T ma;
    int mi = -1;
    for(int i = left; i <= right; i++) {
      T cst = f(line, i);
      if(mi == -1 || comp(cst, ma)) {
        ma = cst;
        mi = i;
      }
    }
    dp[line] = make_pair(mi, ma);
    dfs(top, line - 1, left, mi);
    dfs(line + 1, bottom, mi, right);
  };
  dfs(0, H - 1, 0, W - 1);
  return dp;
}

検証

AtCoder COLOCON -Colopl programming contest 2018- Final C - スペースエクスプローラー高橋君

int main() {
  int N;
  cin >> N;
  vector< int64_t > A(N);
  for(auto &p : A) cin >> p;
  function< int64_t(int, int) > f = [&](int i, int j) { return A[j] + 1LL * (j - i) * (j - i); };
  for(auto &p : monotone_minima(N, N, f)) cout << p.second << endl;
}

応用: オンライン・オフライン変換

動的計画法高速化テクニック(オフライン・オンライン変換)- Qiita

$dp[j] = \min \{ dp[i] + f(i,j) : i \in [0,j) \}$ (例: $f(i,j)$ を区間 $[i,j)$ のコストとすると、区間[0, j) を任意個に分割するときの最小コスト) を考える。

愚直にDPをすると $O(N^2)$ かかるが、最小値をとる位置 $i$ が、 $j$ の増加にしたがって単調増加する場合 $O(N \log^2 N)$ で解くことができる。

区間 $[l, r)$ の dp 配列を埋めたい。$m = \frac {l + r} 2$ とする。 遷移するときに $m$ をまたがないものは分割して再帰的に解くことにすると、$m$ をまたぐものだけを考えれば良い。$[l, m)$ の dp 配列の値はわかり、それ以前の dp の計算結果に依存しない形となる。つまり $2$ 変数関数 $g(j, i) = dp[i] + f(i, j) (l \le i \lt m, m \leq j \lt r)$ を定義できて、Monotone-Minima を用いて効率的に解くことができる。(日本語むずかしい こまった)(分割統治FFTも同じ考え方なので下のコードのinduceの部分をFFTにするとできると思う)

  • online_offline_dp($N$, $f$, $comp$): 長さ $N + 1$ の dp 配列($dp[j]:=$ 区間 $[0, j)$ を任意個に分割するときのコスト) を返す。$f(i, j)$ は区間 $[i, j)$ のコストを与える $2$ 変数関数($0 \leq i \lt j \leq N$ を満たす範囲で定義されていればよい)。
template< typename T, typename Compare = less< T > >
vector< T > online_offline_dp(int W, const function< T(int, int) > &f, const Compare &comp = Compare()) {
  vector< T > dp(W + 1);
  vector< int > isset(W + 1);
  int y_base = -1, x_base = -1;
  function< T(int, int) > get_cost = [&](int y, int x) { // return dp[0, x+x_base)+f[x+x_base, y+y_base)
    return dp[x + x_base] + f(x + x_base, y + y_base);
  };
  function< void(int, int, int) > induce = [&](int l, int m, int r) { // dp[l, m) -> dp[m, r)
    x_base = l, y_base = m;
    auto ret = monotone_minima(r - m, m - l, get_cost, comp);
    for(int i = 0; i < ret.size(); i++) {
      if(!isset[m + i] || comp(ret[i].second, dp[m + i])) {
        isset[m + i] = true;
        dp[m + i] = ret[i].second;
      }
    }
  };
  function< void(int, int) > dfs = [&](int l, int r) {
    if(l + 1 == r) {
      x_base = l, y_base = l;
      T cst = l ? get_cost(0, -1) : 0;
      if(!isset[l] || comp(cst, dp[l])) {
        isset[l] = true;
        dp[l] = cst;
      }
    } else {
      int mid = (l + r) / 2;
      dfs(l, mid);
      induce(l, mid, r);
      dfs(mid, r);
    }
  };
  dfs(0, W + 1);
  return dp;
};

yukicoder No.705 ゴミ拾い Hard

int main() {
  int n;
  cin >> n;
  vector< int > a(n), x(n), y(n);
  for(int i = 0; i < n; i++) cin >> a[i];
  for(int i = 0; i < n; i++) cin >> x[i];
  for(int i = 0; i < n; i++) cin >> y[i];
  function< int64_t(int, int) > dist = [&](int i, int j) {
    assert(0 <= i && i < j && j <= n);
    int s = abs(a[j - 1] - x[i]);
    int t = abs(y[i]);
    return 1LL * s * s * s + 1LL * t * t * t;
  };
  cout << online_offline_dp(n, dist).back() << endl;
}