yutasの競技プログラミング勉強帖

競技プログラミングの問題についての解説記事を主に書いています。

D - Island Tour / AtCoder Beginner Contest 338

問題

 n 個の島からなる諸島があり、これらの島々は  n 本の橋によって結ばれている。

島には  1 から  n までの番号が付けられており、また橋にも  1 から  n までの番号が付けられている。 ここで、  1 \leq i \leq n - 1 である橋  i は島  i と島  i + 1 を結んでおり、橋  n は島  n と島  1 を結んでいる。 また、橋を渡る以外の手段で島を行き来する手段は存在しない。

この諸島に対して、島  {x}_{1}, \, {x}_{2}, \, \cdots , \, {x}_{m} を順に訪れるツアーが開催されており、このツアーの長さを「行き来する橋の本数」として定義されている。

厳密には、ツアーとは以下の条件をすべて満たす  l + 1 個の島の列  {a}_{0}, \, {a}_{1}, \, \cdots , \, {a}_{l} のことを指す。

  • すべての  j (ただし、  0 \leq j \leq l - 1)に対して、島  {a}_{j} と島  {a}_{j + 1} が直接橋で結ばれている。
  • ある  0 = {y}_{1} \lt {y}_{2} \lt \cdots \lt {y}_{m} = l が存在して、すべての  k に対して  {a}_{ {y}_{k} } = {x}_{k} が成立する。

この諸島にて、維持費削減のために橋を1本封鎖することになった。 封鎖する橋をうまく選んだときの、ツアーの長さの最小値を求めよ。

入力

まず最初の1行目に、整数  n, \, m が与えられる。

次の1行に、  m 個の整数  {x}_{1}, \, {x}_{2}, \, \cdots , \, {x}_{m} が順に与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  3 \leq n \leq 2 \times {10}^{5}
  •  2 \leq m \leq 2 \times {10}^{5}
  •  1 \leq {x}_{k} \leq n
  •  {x}_{k} \ne {x}_{k + 1} (ただし、  1 \leq k \leq m - 1

出力

答えとなる最小値を1行で出力すること。

解法

まず、橋を1本も封鎖しなかったときのツアーの長さの最小値について考えます。

そのために、島  {x}_{k} から島  {x}_{k + 1} へ移るときについて着目します。 (簡単のため、  {x}_{k} \lt {x}_{k + 1} とします。)

このとき、島  {x}_{k} から島  {x}_{k + 1} への移り方としては、

  1.  {x}_{k}, \, {x}_{k} + 1, \, \cdots , \, {x}_{k + 1} の順に移る
  2.  {x}_{k}, \, {x}_{k} - 1, \, \cdots , \, 1 , \, n ,\, n - 1, \, \cdots , \, {x}_{k + 1} の順に移る

の2通りが考えられます。

このそれぞれの場合の長さについては、1つ目の方法では  {x}_{k + 1} - {x}_{k} となります。 2つ目の方法では、諸島全体で  n 本の橋があり、1つ目の方法では渡らなかったすべての橋を渡るため、  n - ({x}_{k + 1} - {x}_{k}) となります。

従って、このうちの小さい方が、島  {x}_{k} から島  {x}_{k + 1} へ移る際の長さの最小値ということになります。 これをすべての  k (ただし、  1 \leq k \leq m - 1 )に対して計算することによって得られる値の和が、ツアーの長さの最小値となります。

さて、ここから橋を1本封鎖したときのツアーの長さの最小値について考えます。 そのため、ある橋  i を封鎖することについて考えます。

このときに、島  {x}_{k} から島  {x}_{k + 1} へと移る方法を考えます。

先程の方法のうち1つ目の方法で進む方が最短であり、橋  i がその中に含まれている場合では、橋  i が封鎖されることにより、2つ目の方法で進むしかなくなります。 このことにより、全体のツアーの長さは元々の場合と比較して、 \begin{align} (n - ({x}_{k + 1} - {x}_{k})) - ( {x}_{k + 1} - {x}_{k} ) = n - 2({x}_{k + 1} - {x}_{k}) \end{align} だけ大きくなります。

また、先程の方法のうち2つ目の方法で進む方が最短であり、橋  i がその中に含まれている場合では、橋  i が封鎖されることにより、1つ目の方法で進むしかなくなります。 このことにより、全体のツアーの長さは元々の場合と比較して、 \begin{align} ({x}_{k + 1} - {x}_{k}) - (n - ({x}_{k + 1} - {x}_{k})) = 2 ({x}_{k + 1} - {x}_{k}) - n \end{align} だけ大きくなります。

これらの場合に当てはまらない場合、すなわち、最短で移動する方法の中に橋  i が含まれない場合には、橋  i が封鎖されたとしても、全体のツアーの長さの最小値には影響を与えません。

これをすべての  k (ただし、  1 \leq k \leq m - 1 ) に対して計算することで、橋  i が封鎖されることによって、ツアーの長さの最小値からどれだけ値が大きくなるのかを導出することができます。 この値を  \mathrm{cost}_{i} として定義します。

この  \mathrm{cost}_{i} をすべての  i (ただし、  1 \leq i \leq n )に対して導出されたとします。 このときに、  \mathrm{cost}_{i} が最小となるような橋  i を封鎖すると、ツアー全体の長さも最小となると言えます。

従って、与えられた  {x}_{1}, \, {x}_{2}, \, \cdots , \, {x}_{m} をもとに、  \mathrm{cost}_{1}, \, \mathrm{cost}_{2}, \, \cdots , \, \mathrm{cost}_{n} を導出する方法について考えます。

ここで、島  {x}_{k} から島  {x}_{k + 1} に移る場合について考えます。

1つ目の方法で進む方が最短となる場合では、橋  {x}_{k} ,\, {x}_{k} + 1 , \, \cdots , \, {x}_{k + 1} - 1 を使用することになります。 従って、  {x}_{k} \leq i \leq {x}_{k + 1} - 1 であるすべての  i に対して、 \begin{align} \mathrm{cost}_{i} \leftarrow \mathrm{cost}_{i} + (n - 2 ({x}_{k + 1} - {x}_{k})) \end{align} として更新されます。

また、2つ目の方法で進む方が最短となる場合では、橋  {x}_{k} - 1, \, \cdots , \, 1, \, n , \, \cdots , \, {x}_{k + 1} を使用することになります。 従って、  1 \leq i \leq {x}_{k} - 1 または  {x}_{k + 1} \leq i \leq n であるすべての  i に対して、 \begin{align} \mathrm{cost}_{i} \leftarrow \mathrm{cost}_{i} + (2({x}_{k + 1} - {x}_{k}) - n) \end{align} として更新されます。

これをすべての  1 \leq k \leq m - 1 に対して行うことで、すべての  \mathrm{cost}_{i} の値が求まります。

ただし、  \mathrm{cost}_{i} を1つずつ更新する方法では、1つの  k に対して  O(n) 程度の計算量がかかってしまいます。 すなわち、最終的な  \mathrm{cost}_{i} を得るためには、  O(nm) 程度の計算量がかかってしまい、  n \leq 2 \times {10}^{5}, \, m \leq 2 \times {10}^{5} であるため、これは実行時間制限に間に合いません。

この計算量を落とすために、「連続する  i に対して同じ値を加えていく」ということに着目した、imos法 *1 と呼ばれる方法を用います。 (一般的な使い方はリンク先のものをご覧ください。)

この方法では、例えば  {x}_{k} \leq i \leq {x}_{k + 1} - 1 である  i に対して  c だけ  \mathrm{cost}_{i} に加えて更新するとしたとき、 \begin{align} \mathrm{cost}_{ {x}_{k} } & \leftarrow \mathrm{cost}_{ {x}_{k} } + c \\ \mathrm{cost}_{ {x}_{k + 1} } & \leftarrow \mathrm{cost}_{ {x}_{k + 1} } - c \end{align} としてまずは更新を行います。 そして、すべての更新が終わった際に、  i = 1, \, 2, \, \cdots , \, n の順に、 \begin{align} \mathrm{cost}_{i} \leftarrow \mathrm{cost}_{i} + \mathrm{cost}_{i - 1} \end{align} として更新を行っていくことにより、最終的に正しい  \mathrm{cost}_{i} の値を得られることになります。

この方法では、1回あたりの更新が  O(1) の計算量であることから、全体で  O(m + n) の計算量で求めることができます。 従って、実行時間制限に間に合うということが言えます。

このようにして求まった  \mathrm{cost}_{i} のうちの最小値を、橋が封鎖されていない場合の最小値に加えた値を出力することで、この問題を解くことができます。

ソースコード

main() 関数の中に、答えを出力する部分を直接実装しました。

ll n, m, x[int(2e5 + 5)];
ll cost[int(2e5 + 5)];

int main() {
  // 値の入力
  cin >> n >> m;
  for (int i = 1; i <= m; i++) {
    cin >> x[i];
  }

  ll ans = 0; // 橋が1本も壊れていない場合の最小値を表す値 ans

  // i = 1, ..., m - 1 の順にツアーの行程を見ていき、 cost の値を更新していく
  for (int i = 1; i <= m - 1; i++) {
   // x[i] < x[i + 1] となるように、値を交換しておく
    bool swapped = false; // 値を交換したかを表すフラグ swapper
    if (x[i] > x[i + 1]) {
      swap(x[i], x[i + 1]);
      swapped = true; // もし swap した場合は、フラグを true にしておく
    }

    ans += min(x[i + 1] - x[i], (x[i] + n) - x[i + 1]); // 理想的な場合の値を更新する
    ll now = abs((x[i + 1] - x[i]) - (x[i] + n - x[i + 1])); // cost として加える値を表す now

    if (x[i + 1] - x[i] <= (x[i] + n) - x[i + 1]) {
      // 1つ目の方法が最短となる場合は、 x[i], ..., x[i + 1] - 1 の cost が追加される
      cost[x[i]] += now;
      cost[x[i + 1]] -= now;
    } else {
      // 2つ目の方法が最短となる場合は、 1, ..., x[i] - 1, x[i + 1], ..., n の cost が追加される
      cost[1] += now;
      cost[x[i]] -= now;
      cost[x[i + 1]] += now;
      cost[n + 1] -= now;
    }

    // もし値を交換していた場合は、もとに戻しておく
    if (swapped) {
      swap(x[i], x[i + 1]);
    }
  }

  // imos法を用いて cost の値を更新する
  for (int i = 1; i <= n; i++) {
    cost[i] += cost[i - 1];
  }
  sort(cost + 1, cost + n + 1); // cost を昇順に並び替える
  cout << ans + cost[1] << endl; // ans に cost の最小値を足した値を出力する

  return 0;
}