いわゆる区間分割する系の DP。こういう系は高速化を要求するタイプの問題が多いけど、今回は素直な二乗 DP で OK!
問題概要
直線上にカツが 個ある。 個目のカツは位置 にあり、その重さは である。これらのカツを、次のようにしてすべて回収する。
- 直線上に 2 点 A, B を配置する (コスト 1)。配置した点上にカツがあるならば、カツを回収する
- 2 点を動かしていく。距離 1 動かすのに要するコストは、1 + (回収したカツの重さの総和) となる
- 2 点が一致したら、コスト を支払って 2 点を回収する
すべてのカツを回収するのに要するコストの最小値を求めよ。
制約
考えたこと
一回の操作で回収するコストは連続する区間になる。よって、区間ごとに分割するタイプの DP で解けそう。つまり、
- := 区間 のカツをすべて回収する最小コスト
として、
- =
という感じで行ける。ここで は、A をカツ に、B をカツ に配置したときに、カツ をすべて回収するための最小コストとする。
f(l, r) を求める
よって、区間 のカツをすべて回収するのに必要なコストを計算する問題へと帰着された。
2 点 A, B をどこで合流させるかを考えることになる。基本的には、カツがあるところに合流させればよい。よって、 のいすれかのカツのところで 2 点 A, B を合流させる。つまり、
- カツ から右へと移動してカツ まで移動するのにかかるコストを
- カツ から左へと移動してカツ まで移動するのにかかるコストを
と表すと、 となる。そして少し考えると、
- を固定したとき、 を最小にするような は に対して単調増加
ということがいえる。よって、しゃくとり法を用いることで、 テーブル全体を求めるのに要する計算量は になる。
#include <bits/stdc++.h> using namespace std; template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; } template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; } int N, K; vector<long long> X, W; long long solve() { vector<vector<long long>> cost(N+1, vector<long long>(N+1, 0)); vector<long long> sumW(N+1, 0), sum(N+1, 0); for (int i = 0; i < N; ++i) { sumW[i+1] = sumW[i] + W[i]; sum[i+1] = sum[i] + sumW[i+1] * (X[i+1] - X[i]); } auto g = [&](int l, int r) { return sum[r] - sum[l] - sumW[l] * (X[r] - X[l]); }; auto h = [&](int l, int r) { return (sumW[r+1] - sumW[l]) * (X[r] - X[l]) - g(l, r); }; auto f = [&](int l, int r, int k) { return g(l, k) + h(k, r); }; for (int l = 0; l < N; ++l) { int k = l; for (int r = l; r < N; ++r) { while (k < r && f(l, r, k) > f(l, r, k + 1)) ++k; cost[l][r] = f(l, r, k) + (X[r] - X[l]) + K + 1; } } vector<long long> dp(N+1, 1LL<<60); dp[0] = 0; for (int i = 1; i <= N; ++i) { for (int j = 0; j < i; ++j) { chmin(dp[i], dp[j] + cost[j][i-1]); } } return dp[N]; } int main() { cin >> N >> K; X.assign(N+1, 0), W.assign(N+1, 0); for (int i = 0; i < N; ++i) cin >> X[i] >> W[i]; cout << solve() << endl; }