noshi91のメモ

データ構造のある風景

ICPC2019国内予選D Ο(NlogN)

予選落ちました



本番で AC しましたが、証明の誤りがありましたらご指摘をお願いします。

解法


c=a-b をして階差を取ると、次のような問題に帰着できます。
c[0]...c[n] があります。「c[i]+=1, c[j]-=1 (i<j)」を最小回数行って c を mod m で 0 にしてください。

 これを左から走査する方法で考えてみます。 すると c[i]+=1 はしたけれども j が決まってない、「未消化の操作」をいくつか貯め込みながら操作を決めていくことになります。 この未消化の操作の個数を rem として、rem の挙動について考えてみましょう。
c[i] を 0 にするには 2 通りの方法があります。

タイプ1: 新たに操作を行い、c[i]+=1 を m-c[i] 回行う、ans+=m-c[i], rem+=m-c[i]
タイプ2: 未消化の操作を消費し、c[i]-=1 を c[i] 回行う、rem-=c[i]

+ と - が混在する場合その 2 操作を結合できることから混在は起きません。 + や - を m 回以上するなら最後尾に寄せればよく、考慮の必要がありません。

 さて、タイプ2 は ans を増加させませんから、出来る限りタイプ2 で済ませたいです。 しかしタイプ2 は rem>=c[i] でしか使えないため、rem が足りない場合タイプ1 を使う必要が出てきます。 そこで、rem が足りなくなった時は過去に行ったタイプ2 をタイプ1 に変更することで rem を補充します (もちろん、i をタイプ1 で処理することも考慮する必要があります)。

 k についてタイプ2 をタイプ1 に切り替えると、rem+=m, ans+=m-c[k] の効果があります。 どの k についてタイプを変えるかで rem の増加量は変わらず、ans の増加量は変わることに注意してください。 ここで rem の挙動をよく考えると、「i で rem が足りなくなったならば、i 以前 (i 含む) のタイプ2 のどれかはタイプ1 に切り替えるしかない」が分かります。 これ及び前述した性質から「i 以前のタイプ2 のうち m-c[i] が最小であるものを貪欲に変更すればよい」ことが得られます。

 以上より、タイプ2 の i について m-c[i] を priority_queue などで管理することで Ο(NlogN) でこの問題が解けました。 なお、m-c[i] が m 以下の自然数であることから y-fast-trie などのデータ構造を用いて expected Ο(NloglogM) で解くことも可能です。

#include <cstddef>
#include <cstdint>
#include <functional>
#include <iostream>
#include <queue>
#include <vector>

int main() {
  using usize = std::size_t;
  using u64 = std::uint_fast64_t;

  usize n;
  u64 m;
  std::cin >> n >> m;
  std::vector<u64> c(n + 1);
  for (usize i = 0; i != n; ++i) {
    u64 a;
    std::cin >> a;
    c[i] = a;
  }
  for (usize i = 0; i != n; ++i) {
    u64 b;
    std::cin >> b;
    c[i] -= b;
  }
  for (usize i = n; i != 0; --i) {
    c[i] -= c[i - 1];
    c[i] = (c[i] + m + m) % m;
  }

  std::priority_queue<u64, std::vector<u64>, std::greater<u64>> pq;

  u64 ans = 0;
  u64 rem = 0;

  for (const auto e : c) {
    pq.push(m - e);
    if (e > rem) {
      ans += pq.top();
      pq.pop();
      rem += m;
    }
    rem -= e;
  }

  std::cout << ans << std::endl;
}