競プロを頑張るブログ

問題を解いてる時の思考なりをまとめていきます

yukicoder1077を解いた

はじめに (ネタバレ無し)

こんにちは
ひぽたふです。

解きながらの考察、実際の解法をまとめておこうかなと思います。

今回解いた問題はこれです。

No.1077 Noelちゃんと星々4 - yukicoder

星2.5の問題です。
なんとか解けた、という感じの問題でした。

以下はネタバレがあるので見る際は注意してください。

解きながらの考察 (ネタバレ 方針まで)

ここからはネタバレがあります。 問題を解いている際に考えていたことをまとめておこうかなと思います。












問題概要

点がN個与えられる。
iの高さはY_{i}である。

点が広義単調増加(全て Y_{i} \leq Y_{i + 1} )になるように変化させたい。
動かす必要のある距離の総和の最小値を求めよ。

考察

単調増加になるようなパターンを全部検討できると話が早いね。
どこの点を動かしているかと、今まで動かした点の最高位置が分かれば他の点についても検討できるかな?

制約は・・・点の数が103と、動ける範囲が104かあ。
愚直にDPを考えるとO(Nmax(Y))で107に収まりそうだね。

点を動かせる範囲がY通り考えられるから、さらにO(max(Y))が追加でかかってちょっとはみ出す感じかな。

分岐が複雑じゃなさそうだから、内部処理を高速化できるかな?
とりあえずDPを実装してみるよ。

加算しながら最小値を求める・・・、高速化しにくい形が出てきたかなあ。

具体的にどういう順番で計算しているかを確認してみようかな。
すると、同じ計算を何度もしている部分を発見!

(自分はここでセグ木なり何なりを考えてしまって時間がかかりました。)

最終的によーく計算順番を見て、最小値を求める順番を考えるとDPの内部処理が高速化されてAC!

あとがき + 解法(ネタバレ全開)











DP問題ですね。

動かしたい点(i)と、今まで動かした点の最大値(h)を引数に考えます。
戻り値は最小コストとします。

DPテーブルのサイズはO(Nmax(Y))ですね。

点を動かす最大値がmax(Y)で本当にいいのかも考えておきます。

最大値をmax(Y)より高い方向にずらす場合を考えます。
特定の点をmax(Y) + 1にずらしたい場合というのは、動かしたい点より前にmax(Y) + 1以上の高さが存在する場合です。
そのような点は、それよりも前にmax(Y) + 1以上の高さが存在する必要があり・・・。

開始点まで考えてもmax(Y) + 1以上にある点は存在しないので、点を動かす範囲をmax(Y)までで考えます。

次に分岐を考えます。

点を動かせる範囲は、今までの最大値(h) ~ 取りうる最大値(max(Y))です。
分岐数はO(max(Y))ですね。

ここまで整理すると以下の漸化式になります。
DP[i][h] = min\{DP[i + 1][j] + |Y_{i} - j|, j = h, h + 1 \cdots max(Y)\}

そのため、単純なDPを考えるとO(Nmax(Y)^{2})の計算量がかかってしまいます。
104 + 3 + 3なのでこのままだと厳しいですね。

そこで、内部ループの高速化を考えます。
単純に実装した場合、下図のような計算を行なっています。

f:id:hipotaf:20200613151632p:plain

このDP[i + 1][j] + |Y_{i} - j|のところで毎回同じ計算していることに気がつくと思います。

そのため、予め加算しておくと下図のような計算になります。

f:id:hipotaf:20200613152748p:plain

毎回minを求めているのでまだ高速化できていません。
このminをそれぞれO(1)で求めるために、右側(小さい区間)から計算します。
(自分はここでセグ木を使って最小値を求め始めて泥沼化しました・・・。)

右側から求めた場合、h番目から終端までのminは、min(直前の区間の値, h番目の値)になってO(1)で求めることができますね。

これでDP[i][h]の値がO(1)で求まり、最終的にO(Nmax(Y))となります。

ソースコード

int main() {
  ll input(n);
  vector<ll> y(n);
  REP(i, n) cin >> y[i];

  ll YMAX = 10000;
  vector<vector<ll>> table(n + 1);

  // 初期値
  REP(i, n + 1) {
    if (i == n) table[i] = vector<ll>(YMAX + 1, 0);
    else table[i] = vector<ll>(YMAX + 1);
  }

  // DPの計算
  REPD(i, n) {
    ll minv = LLONG_MAX;
    FORD(h, YMAX, 0) {
      // ここの式が今回の難しいポイントだったと思います
      minv = min(minv, table[i + 1][h] + abs(y[i] - h));
      table[i][h] = minv;
    }
  }

  print(table[0][0]);
}

マクロに関しては別の記事でまとめてあります。

テンプレート - 競プロを頑張るブログ

ループと入出力のみ使用しています。
REPは半開区間、FORは閉区間となっています。

https://yukicoder.me/submissions/496223

DPの終端を0で初期化していますね。
変化させる場所がない時はコストが0だからというお気持ちを込めています。

コードにしてみると簡潔ですね。

それでは、またっ!