ARC073 Many Moves
atcoder.jp
ACPCVC20200120のCで出ました
このセグ木の使い方が少し教育的と感じたため、メモとして記述してみます。
問題文
個のマスがある。コマを2つ持っていて、コマはそれぞれマス にある。マス にどちらかのコマを移動させるというクエリを 個処理する。
クエリを処理した後の、コマの移動距離の最小を求める。
解法
1
それぞれのコマが今どのマスにいるかという情報がわかれば、この先の移動の仕方は同じになることがわかるので、まず以下のDPが作れる。: 個のクエリを処理し終え、現在のコマは に存在する。
現在は となっている。まだ遅い。
2
次に、どちらかのコマの座標は になっていることに着目すると、実はどちらかのコマの情報がいらない。これで変数が2つになる。: 個のクエリを処理し終え、現在のコマは に存在する。
遷移も後に説明するが、ちゃんと で出来るため、 になった。 まだ。
3
実はここからの高速化は、DPの遷移自体を小さくするわけではない。一気に 個遷移することで高速化する。ところで、先ほどのDPの遷移はどうなっていたのか。それは次のようになっている。
一見複雑そうに見えるが、実はそうでもない。
を に動かすか、どこかにある を に動かすかの2択しか無い。
ちゃんと睨めば漸化式が出てくるはず。
ここで、 を に動かす動作の遷移を見てみると次のようになっている。
…から に遷移しているだけ。しかも遷移にかかるコストは で一定となっており配列全体に を足すという処理で 個の遷移を実現できる。
□□□□□□□□□□□□□□□□□□□□□□□
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
□□□□□□□□□□□□□□□□□□□□□□□
…
配列全体に同じものを足す遷移は高速にできるので、適宜工夫すると良い。 この操作自体は で行える(実際はSegmentTreeの影響で 掛かりそう)
4
次に、 の高速な計算方法を説明する。この式は絶対値を外すことで、扱いやすい形になる。
このとき、 と の配列を持っておけば、区間のminを求める操作でこの処理を実現できることがわかる。
これは予めdpのマス に対して や を足しておくことで作ることが出来る。
区間のminを求める操作はSegment Tree を使うと で動作するので高速に動作することが出来る。(2個セグ木をつかう)
これで、全体で となり、間に合わせることが出来る。
このDPをインラインDPというらしい。
topcoder.g.hatena.ne.jp
とても素晴らしいサイトでした。
ソースコード
int main() { ll N, Q, A, B; ll X[200005]; cin >> N >> Q >> A >> B; SegmentTree<ll> segl(N + 3, LLINF, [](ll a, ll b) { return (min(a, b)); }); SegmentTree<ll> segr(N + 3, LLINF, [](ll a, ll b) { return (min(a, b)); }); ll sum = 0; X[0] = A; segl.update(B, -B); segr.update(B, B); for (int i = 1; i <= Q; i++) { cin >> X[i]; ll diff = llabs(X[i] - X[i - 1]); sum += diff; ll val = min(segl.query(1, X[i] + 1) + X[i], segr.query(X[i] + 1, N + 1) - X[i]); segl.update(X[i - 1], val - X[i - 1] - diff); segr.update(X[i - 1], val + X[i - 1] - diff); } ll minv = 0; for (int i = 1; i <= N; i++) { minv = min(minv, segl.query(i, i + 1) + i); } cout << minv + sum << endl; return (0); }
感想
2次元DPの遷移を一気に 回やるっていう方法が知れてよかった。DP、使えるなあ。
あと、ソースコードのフォントサイズ、どうにかならないかな。