Codeforces Round #185 (Div. 1) B. Cats Transport
問題概要
N個の地点があり,順に 0 から N-1 とする.地点 i と i + 1 の間の距離は d[i] である.
また,猫が m 匹いて,猫 j は時刻 t[j] に地点 h[j] にあらわれるものとする.それぞれの猫は,餌をもらえるまで出現してからその地点で待機し続ける.
今,猫に餌を上げる人が p 人いて,それぞれ地点 0 から N-1 の方向に単位時間あたり1だけ移動する.(ただし初期位置は自由.つまり負の時刻に地点 0 を出発するような人がいても良い.これは制約に書いてないが…)
移動中に餌を待っている猫と遭遇した場合,その猫に必ず餌をやる.ただし,餌をやる時間は無視できるものとする.
では,全ての猫に餌をやるように人を配置した時に,それぞれの猫が餌を待つ時間の総和を最小化せよ.
・制約
2 <= N <= 2 * 10^5
1 <= M <= 10^5
1 <= p <= 100
解法
まず,今のままでは位置と時間という軸が2つあり面倒なので,これをひとつにまとめます.人は単位時間に1だけすすむので,時刻 t[j] に地点 h[j] に現れるという情報を a[j] := t[j] - pos[j] とまとめてしまいます(pos[j] は地点 h[j] の座標).a[j] は,時刻 a[j] に人を地点 0 に配置すれば,その人は猫 j が現れるよりあとに通過する(つまり餌をあげることができる)ことを意味します.
こうして得られた a[j] を昇順にソートしておきます.
時刻 a[j] に人を地点 0 に配置した場合,それ以前の直近で人が配置された時刻を a[k] をすると,k + 1 <= j となるような全ての猫について餌をやることになります.
ここからDPパートです.
p が小さいので,以下のような dp が自然に思い浮かびます.
dp[i][j] := i 人目まで使い,かつ i 人目を時刻 a[j] に出発させた時の,j 以前の猫の待ち時間の総和の最小値
このdpの更新は以下のようになります.
$\displaystyle dp[i][j] = \min_{k < j} \{dp[i - 1][k] + \sum_{l=k+1}^{j}(a[j] - a[l])\}$
式を変形すると,以下のようにできます.
$\displaystyle dp[i][j] = \min_{k < j} \{dp[i - 1][k] - k \times a[j] - \sum_{l=k+1}^{j}a[l]\} + j \times a[j]$
さらに累積和 $\displaystyle S[j] = \sum_{l=0}^{j-1} a[l]$を用いて整理すると
$\displaystyle dp[i][j] = \min_{k < j} \{dp[i - 1][k] - k \times a[j] + S[k+1] \} + j \times a[j] - S[j+1]$
このままだと O(pM^2) ですが,この形のDPは ConvexHullTrick という方法で高速化できます.(この方法自体の解説は省略します.)
よって O(pM) でこの問題を解くことができます.
ソースコード
#include <bits/stdc++.h> using namespace std; class convex_hull_trick { using T = long long; bool check(int f1, int f2, T aa, T bb) { return (a[f2] - a[f1]) * (bb - b[f2]) >= (b[f2] - b[f1]) * (aa - a[f2]); } bool check(int f1, int f2, int f3) { return (a[f2] - a[f1]) * (b[f3] - b[f2]) >= (b[f2] - b[f1]) * (a[f3] - a[f2]); } T f(int idx, T x) { return a[idx] * x + b[idx]; } public: convex_hull_trick() = default; // a : sorted sequence convex_hull_trick(std::vector<T> const& a_, std::vector<T> const& b_) { assert(a_.size() == b_.size()); for(int i = 0; i < a_.size(); ++i) { add_f(a_[i], b_[i]); } } void add_f(T aa, T bb) { while(a.size() >= 2 && check(a.size() - 2, a.size() - 1, aa, bb)) { a.pop_back(); b.pop_back(); } a.push_back(aa); b.push_back(bb); } T min(T x) { assert(a.size() > 0); while(a.size() >= 2 && f(0, x) >= f(1, x)) { a.pop_front(); b.pop_front(); } return a[0] * x + b[0]; } private: std::deque<T> a, b; // functions }; using namespace std; using ll = long long; constexpr ll INF = 2e18; int main() { ll n, m, p; cin >> n >> m >> p; vector<ll> d(n - 1); for(int i = 0; i < n - 1; ++i) { cin >> d[i]; } vector<ll> pos(n); for(int i = 0; i < n - 1; ++i) { pos[i + 1] = pos[i] + d[i]; } vector<ll> x(m); for(int i = 0; i < m; ++i) { ll h, t; cin >> h >> t; x[i] = t - pos[h - 1]; } sort(x.begin(), x.end()); vector<ll> sum(m + 1); for(int i = 0; i < m; ++i) { sum[i + 1] = sum[i] + x[i]; } vector<vector<ll>> dp(p + 1, vector<ll>(m, INF)); vector<convex_hull_trick> cht(p + 1); for(int i = 0; i < m; ++i) { dp[1][i] = i * x[i] - sum[i]; cht[1].add_f(-i, dp[1][i] + sum[i + 1]); } for(int i = 2; i <= p; ++i) { for(int j = 0; j < m; ++j) { if(j == 0) { dp[i][j] = 0; } else { dp[i][j] = cht[i - 1].min(x[j]) + j * x[j] - sum[j + 1]; } cht[i].add_f(-j, dp[i][j] + sum[j + 1]); } } cout << dp[p][m - 1] << endl; }
感想
ConvexHullTrickを初めて書いたので大変だった.かなり典型問題なので勉強になりました.