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;
}

C - Leftover Recipes / AtCoder Beginner Contest 338

問題

冷蔵庫に  n 種類の材料があり、これらは材料  1, \, \cdots , \, n と番号が付けられている。 材料  i {q}_{i} グラムある。

ここで、2種類の料理が作れる。 料理Aは、1人分を作るのに各材料  i {a}_{i} グラム必要であり、料理Bは、1人分を作るのに  {b}_{i} グラム必要である。 また、どちらも整数人分しか作ることができない。

このとき、冷蔵庫にある材料のみを使って、最大で合計何人分の料理を作れるか求めよ。

入力

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

次の1行には、  n 個の整数  {q}_{1}, \, {q}_{2}, \, \cdots , \, {q}_{n} が空白区切りで与えられる。

次の1行には、  n 個の整数  {a}_{1}, \, {a}_{2}, \, \cdots , \, {a}_{n} が空白区切りで与えられる。

次の1行には、  n 個の整数  {b}_{1}, \, {b}_{2}, \, \cdots , \, {b}_{n} が空白区切りで与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  1 \leq n \leq 10
  •  1 \leq {q}_{i} \leq {10}^{6}
  •  0 \leq {a}_{i} \leq {10}^{6}
    •  {a}_{i} \geq 1 であるような  i が存在する。
  •  0 \leq {b}_{i} \leq {10}^{6}
    •  {b}_{i} \geq 1 であるような  i が存在する。

出力

最大で合計  s 人分の料理を作れるとき、1行で整数  s を出力すること。

解法

まず、料理Bを作らずに料理Aのみを作る場合について考えます。 このとき、料理Aを最大で何人分作れるのか、ということを考えます。

これは、  {a}_{i} \ne 0 である各材料に対して  \displaystyle \left\lfloor \frac{ {q}_{i} }{ {a}_{i} } \right\rfloor の値を求めていき、その中の最小値が答えとなります。 というのも、すべての材料が揃っていないと料理を作ることができないためです。

従って、この値を  {s}_{a} として、以下料理Bも作る場合について考えます。

ここで、料理Aを  i 人分作るときについて考えます。(ただし、  0 \leq i \leq {s}_{a} とします。)

このとき、材料  j ({q}_{j} - i \cdot {a}_{j}) だけ余っています。 そのため、料理Bを最大で何人分つくれるのか、ということについては先程と同様に、 {b}_{j} \ne 0 である各材料に対して、  \displaystyle \left\lfloor \frac{{q}_{j} - i \cdot {a}_{j}}{{b}_{j}} \right\rfloor の値を求めていき、その中の最小値が答えとなります。

このようにして、料理Aを  i 人分作るときの料理Bを作れる最大値を求めていき、その和の最大値を  i = 0, \, 1, \, \cdots , \, {s}_{a} の範囲で求めることによって、この問題の答えが得られます。

計算量については、料理Aを最大で  {s}_{a} 人分作れるとしたときに、  O({s}_{a} n) で求めることができます。 この問題では  n \leq 10 であり、  \displaystyle {s}_{a} \leq \frac{{q}_{i}}{{a}_{i}} \leq {10}^{6} であるので、これは実行時間制限に十分間に合うと言えます。

ソースコード

まず、料理Aを何人分作るのかということを引数として、最終的に作ることのできる人数の合計値を返す関数 solve() を以下のように実装しました。

int n;
ll q[15], a[15], b[15];

ll solve(ll a_cnt) { // a_cnt は料理Aを作る人数を表す
  ll b_cnt = 1e9; // b_cnt は料理Bを作る人数を表す

  // i = 1, ..., n の順に、b_cnt の値を更新していく
  for (int i = 1; i <= n; i++) {
    ll rest = q[i] - a[i] * a_cnt; // 材料 i の残りの量を表す rest

    if (b[i] == 0) {
      // b[i] = 0 のときは考慮する必要がない
      continue;
    }
    b_cnt = min(b_cnt, rest / b[i]); // b_cnt の値を更新する
  }

  return a_cnt + b_cnt; // 最終的に a_cnt と b_cnt の和を返す
}

この solve() 関数が実装された状態で、 main() 関数の中に、答えを出力する部分を直接実装しました。

int n;
ll q[15], a[15], b[15];

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

  ll a_max = 1e9; // 料理Aを作れる人数の最大値を表す a_max
  for (int i = 1; i <= n; i++) {
    if (a[i] == 0) {
      // a[i] = 0 のときは考慮する必要はない
      continue;
    }
    a_max = min(a_max, q[i] / a[i]); // a_max の値を更新する
  }

  ll ans = 0; // 最終的な答えを表す ans

  // i = 0, ..., a_max について、作れる料理の個数を計算して ans を更新する
  for (ll i = 0; i <= a_max; i++) {
    ans = max(ans, solve(i));
  }
  cout << ans << endl;

  return 0;
}

B - Frequency / AtCoder Beginner Contest 338

問題

英小文字からなる文字列  s が与えられる。  s に最も多く出現する文字を求めよ。 ただし、そのような文字が複数ある場合は、そのうちアルファベット順で最も早いものを答えよ。

入力

1行に、文字列  s が与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  1 \leq | s | \leq 1000
  •  s の各文字は英小文字である。

出力

 s に最も多く出現する文字のうち、アルファベット順で最も早いものを1行で出力すること。

解法

文字列  s の各文字について順番に見ていき、 a から z までの文字がそれぞれ何回出現したのかを数えていくことがまずは必要です。

この数え方の管理方法としては、アルファベットは全部で26文字であることから、 cnt[26] のような26個分の配列を用意しておき、「 a からいくつ離れているもののカウントを1つ増やす」という方法で行うことができます。 例えば C++ では、文字 s[i] のカウントを1つ増やしたいときについては、 cnt[s[i] - 'a']++; によってこの操作が可能になります。

以上によって得られた cnt[26] に対して、順番に値を見ていき、そのうち最大かつ出現が最も早いものを出力すれば、この問題の答えが得られます。

この方法は  O(|s|) の計算量で答えが得られるので、実行時間制限にも十分間に合うと言えます。

ソースコード

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

string s;
int m = 0, cnt[26]; // m は cnt[i] の最大値を表す

int main() {
  // 値の入力
  cin >> s;

  // s の各文字について見ていき、 cnt の値を更新していく
  for (int i = 0; i < s.length(); i++) {
    cnt[s[i] - 'a']++;
  }

  char ans; // 答えとなる文字を表す ans  

  // i = 0, ..., 25 の順に、cnt[i] の値を見ていく
  for (int i = 0; i < 26; i++) {
    if (m < cnt[i]) {
      // m が cnt[i] より小さいとき、 m と ans を更新する
      m = cnt[i];
      ans = 'a' + i;
    }
  }
  cout << ans << endl;

  return 0;
}

A - Capitalized? / AtCoder Beginner Contest 338

問題

英大文字・英小文字からなる空でない文字列  s が与えられる。 この  s が以下の条件を満たしているか判定せよ。

  •  s の先頭の文字は大文字であり、それ以外の文字はすべて小文字である。

入力

1行に、文字列  s が与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  1 \leq | s | \leq 100
  •  s の各文字は英大文字または英小文字である。

出力

条件が満たされていれば Yes を、そうでなければ No を出力すること。

解法

文字列  s のうち、「 s[0] にあたる部分が大文字かつ、それ以外の部分はすべて小文字」という条件を満たしているかを、  s の先頭から順番に各文字を見ていくことによって、この問題を解くことができます。

ここで、 s[0] が大文字かどうかの判定について C++ では 'A' <= s[0] && s[0] <= 'Z' の条件式によって行えます。 同様に、 s[i] が小文字かどうかの判定については 'a' <= s[i] && s[i] <= 'z' によって行えます。

以上を用いることによって、  O(|s|) の計算量で条件を満たすか否かの判定を行えます。

ソースコード

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

int main() {
  // 値の入力
  string s;
  cin >> s;

  bool flag = true; // flag は true のときに条件を満たし、 false のときに条件を満たさない

  if ('A' <= s[0] && s[0] <= 'Z') {
    // 先頭から2番目の文字から順番に、小文字かどうかを見ていく
    for (int i = 1; i < s.length(); i++) {
      if (!('a' <= s[i] && s[i] <= 'z')) {
        flag = false; // s[i] が小文字でないときは flag を false にする
      }
    }
  } else {
    flag = false; // 先頭が大文字でないときは flag を false にする
  }

  // 答えの出力
  if (flag) {
    Yes();
  } else {
    No();
  }

  return 0;
}

F - Sum of Progression / Codeforces Round 920 (Div. 3)

問題

 n 個の整数からなる数列  a が与えられる。 また、整数  s, \, d, \, k によって構成されるクエリが  q 個与えられる。

 q 個のクエリについて、  {a}_{s} + 2 \cdot {a}_{s + d} + \cdots + k \cdot {a}_{s + d \cdot (k - 1)} の値を求めよ。

入力

まず最初の1行目に、テストケースの個数を表す整数  t が与えられる。

その後、  t 個のテストケースのそれぞれに対して、以下のように入力が与えられる。

  • 最初の1行目には、整数  n, \, q が与えられる。
  • 次の1行には、  n 個の整数  {a}_{1}, \, {a}_{2}, \, \cdots , \, {a}_{n} が与えられる。
  • 次の  q 行のうち  i 行目には、  i 番目のクエリを表す整数  s, \, d, \, k が与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  1 \leq t \leq {10}^{4}
  •  1 \leq n \leq {10}^{5}
    • すべてのテストケースにおける  n の値の合計は  {10}^{5} を超えない。
  •  1 \leq q \leq 2 \cdot {10}^{5}
    • すべてのテストケースにおける  q の値の合計は  2 \cdot {10}^{5} を超えない。
  •  - {10}^{8} \leq {a}_{i} \leq {10}^{8}
  •  1 \leq s, \, d, \, k \leq n
  •  s + d \cdot (k - 1) \leq n

出力

各テストケースについて、  q 個のクエリの答えを順番に空白区切りで1行ずつ出力すること。

解法

まず、クエリについて  s, \, d, \, k が与えられた際に、定義通りに  {a}_{s} + 2 \cdot {a}_{s + d} + \cdots + k \cdot {a}_{s + d \cdot (k - 1)} を計算した場合の計算量について考えます。

このとき、  k 個の項を利用することから、値を導出するには  O(k) の計算量が必要になります。

ここで問題の条件から、  k の値の最大値は  s, \, d の値によって決定されます。 すなわち、 \begin{align} s + d \cdot (k - 1) \leq n \end{align} が成立するため、この式を整理すると、 \begin{align} k \leq \frac{n - s}{d} + 1 \end{align} が成立することから、定義通りの計算では   \displaystyle O \left( \frac{n}{d} \right) の計算量が必要になると言えます。

このことから、すべての  d の値が十分大きいときについてはこの計算を行うのみで実行時間制限に間に合いますが、そうでないときについては実行時間制限には間に合わないと言えます。

そこで、  d の値が小さいときについて、計算量を減らしながらこの計算を行う方法について考えます。 ここで、値  \mathrm{sum}_{i, \, j}, \, \mathrm{dp}_{i, \, j} をそれぞれ \begin{align} \mathrm{sum}_{i, \, j} & = {a}_{i} + {a}_{i + j} + {a}_{i + 2j} + \cdots \\ \mathrm{dp}_{i, \, j} & = {a}_{i} + 2 \cdot {a}_{i + j} + 3 \cdot {a}_{i + 2j} + \cdots \end{align} として定義します。 このとき、まず  \mathrm{sum}_{i, \, j} については、 \begin{align} \mathrm{sum}_{i, \, j} & = {a}_{i} + {a}_{i + j} + {a}_{i + 2j} + \cdots \\ & = {a}_{i} + \left( {a}_{i + j} + {a}_{(i + j) + j} + \cdots \right) \\ & = {a}_{i} + \mathrm{sum}_{i + j, \, j} \end{align} が成立します。 従って、  i = n, \, \cdots , \, 1 の順に、  j = 1, \, 2, \, \cdots に対して  \mathrm{sum}_{i, \, j} の値を求めていくことができます。

また、  \mathrm{dp}_{i, \, j} の値については、 \begin{align} \mathrm{dp}_{i, \, j} & = {a}_{i} + 2 \cdot {a}_{i + j} + 3 \cdot {a}_{i + 2j} + \cdots \\ & = {a}_{i} + \left( {a}_{i + j} + {a}_{i + 2j} + \cdots \right) + \left( {a}_{i + j} + 2 \cdot {a}_{i + 2j} + \cdots \right) \\ & = {a}_{i} + \left( {a}_{i + j} + {a}_{(i + j) + j} + \cdots \right) + \left( {a}_{i + j} + 2 \cdot {a}_{(i + j) + j} + \cdots \right) \\ & = {a}_{i} + \mathrm{sum}_{i + j, \, j} + \mathrm{dp}_{i + j, \, j} \end{align} が成立します。 従って、  \mathrm{sum}_{i, \, j} が求まっている状態で、  i = n, \, \cdots , \, 1 の順に、  j = 1, \, 2, \, \cdots に対して  \mathrm{dp}_{i, \, j} の値を求めていくことができます。

この  \mathrm{sum}_{i, \, j}, \, \mathrm{dp}_{i, \, j} が計算されている状態にて、クエリが与えられたときについては、 \begin{align} \, & {a}_{s} + 2 \cdot {a}_{s + d} + \cdots + k \cdot {a}_{s + d \cdot (k - 1)} \\ = & \left( {a}_{s} + 2 \cdot {a}_{s + d} + \cdots + k \cdot {a}_{s + d \cdot (k - 1)} + (k + 1) \cdot {a}_{s + d \cdot k} + \cdots \right) - \left( (k + 1) \cdot {a}_{s + d \cdot k} + \cdots \right) \\ = \, & \mathrm{dp}_{s, \, d} - \left( (k + 1) \cdot {a}_{s + d \cdot k} + (k + 2) \cdot {a}_{s + d \cdot (k + 1)} + \cdots \right) \\ = \, & \mathrm{dp}_{s, \, d} - \left\{ \left( k \cdot {a}_{s + d \cdot k} + k \cdot {a}_{s + d \cdot (k + 1)} + \cdots \right) + \left( {a}_{s + d \cdot k} + 2 \cdot {a}_{s + d \cdot (k + 1)} + \cdots \right) \right\} \\ = \, & \mathrm{dp}_{s, \, d} - k \cdot \mathrm{sum}_{s + d \cdot k , \, d} - \mathrm{dp}_{s + d \cdot k, \, d} \end{align} として整理が行えるので、これは  O(1) の計算量によって求めることができます。

一方で、  \mathrm{dp}_{i, \, j} j の値は最大で  n - 1 まで必要になるため、すべての  \mathrm{dp}_{i, \, j} の値を求めるには  O( {n}^{2} ) の計算量がかかってしまい、これは実行時間制限には間に合いません。

以上から、  j の値の最大値を  b とおき、与えられたクエリの  d の値が  b 以上ならば直接計算し、  b 以下ならば  \mathrm{dp}, \, \mathrm{sum} の値を用いて計算することを考えます。

このとき、全体での計算量は  \displaystyle O \left( q \cdot \frac{n}{b} + bn \right) ということになります。

これを最小化する  b の値については、相加平均・相乗平均の大小関係より、 \begin{align} \frac{nq}{b} & = bn \\ \therefore \; b & = \sqrt{q} \end{align} ということになります。  b がこの値のとき、 \begin{align} q \cdot \frac{n}{b} + bn = q \cdot \frac{n}{\sqrt{q}} + \sqrt{q} \cdot n = 2 n \sqrt{q} \end{align} であるので、 n \sqrt{q} \leq 2 \sqrt{5} \cdot {10}^{7} より、これは実行時間制限に間に合うということが言えます。

この問題では  q \leq 2 \cdot {10}^{5} より、  \sqrt{q} \leq 2 \sqrt{5} \cdot {10}^{2} であるので、  b = 300 辺りに固定すれば、  q 個のクエリに実行時間制限に間に合いながら答えることができると言えます。

ソースコード

各テストケースに対し、  q 個のクエリの答えを出力する関数 solve() を以下のように実装しました。

int n, q;
ll s, d, k;
ll a[int(1e5 + 5)], ans[int(2e5 + 5)]; // ans[i] は i 番目のクエリの答えを示す

const int b = 300;
ll dp[int(1e5 + 305)][305], sum[int(1e5 + 305)][305];

void solve() {
  // 値の入力(実行時間制限オーバーを防ぐために、 scanf を使用する)
  scanf("%d %d", &n, &q);
  for (int i = 1; i <= n; i++) {
    scanf("%lld", a + i);
  }

  // i = n, ..., 1 の順に sum, dp の値を更新する
  for (int i = n; i >= 1; i--) {
    for (int j = 1; j <= b; j++) {
      sum[i][j] = sum[i + j][j] + a[i]; // sum[i] は sum[i + j] の値を使用して更新する
      dp[i][j] = dp[i + j][j] + sum[i][j]; // dp[i] は dp[i + j] の値を使用して更新する
    }
  }

  // q 個のクエリの答えを順番に計算していく
  for (int i = 1; i <= q; i++) {
    // クエリの入力
    scanf("%lld %lld %lld", &s, &d, &k);

    if (d <= b) {
      // 入力された d が b 以下の場合は dp, sum を使用して計算する
      ans[i] = dp[s][d];
      if (s + k * d <= n) {
        // 次のインデックスが n 以下の場合のみ、 dp, sum の値から引く
        ans[i] -= dp[s + k * d][d] + k * sum[s + k * d][d];
      }
    } else {
      // 入力された d が b より大きい場合は k 個分の値を順番に加えていく
      for (int j = 0; j < k; j++) {
        ans[i] += ll(j + 1) * a[s + j * d];
      }
    }
  }

  // 答えの出力
  for (int i = 1; i <= q; i++) {
    printf("%lld", ans[i]); // 実行時間制限オーバーを防ぐために、 printf を使用する
    if (i == q) {
      printf("\n");
    } else {
      printf(" ");
    }
  }
  return;
}

感想

ABC335 の F問題 を最近扱いましたが、まさかここでも 平方分割 の問題が出てくるとは思いませんでした。

値の整理の方法についてはやや複雑になる部分もありますが、計算量を減らすために工夫する部分についてはほとんど同じですね。

E - Eat the Chip / Codeforces Round 920 (Div. 3)

問題

アリスとボブが格子状のボードでゲームをする。

ボードは  h w 列のマスがあり、上から順に  1, \, 2, \, \cdots , \, h 行、左から順に  1, \, 2, \, \cdots , \, w 列とそれぞれ番号が付けられている。

はじめ、アリスの駒はマス  ( {x}_{a}, \, {y}_{a} ) に、ボブの駒はマス  ( {x}_{b}, \, {y}_{b} ) に位置しており、アリスから交互に駒を動かす。

ここで、アリスの駒がマス  ({x}_{a}, \, {y}_{a}) にあるとき、アリスは駒をマス  ({x}_{a} + 1, \, {y}_{a}), \, ({x}_{a} + 1, \, {y}_{a} - 1), \, ({x}_{a} + 1, \, {y}_{a} + 1) のいずれかに動かすことができる。 また、ボブの駒がマス  ({x}_{b}, \, {y}_{b}) にあるとき、ボブは駒をマス  ({x}_{b} - 1, \, {y}_{b}), \, ({x}_{b} - 1 , \, {y}_{b} - 1), \, ({x}_{b} - 1, \, {y}_{b} + 1) のいずれかに動かすことができる。

一方のプレイヤーがもう一方のプレイヤーの駒と同じマスに位置したとき、その駒を動かしたプレイヤーが勝者となる。 また、いずれかのプレイヤーが駒を動かせなくなったとき(すなわち、アリスが  h 行に到達するか、ボブが  1 行に到達するとき)、このゲームは引き分けとなる。

どちらのプレイヤーも最適な行動を行うとき、このゲームの結果はどうなるか判定せよ。

入力

まず最初の1行目に、テストケースの個数を表す整数  t が与えられる。

その後、  t 個のテストケースのそれぞれに対して、整数  h, \, w, \, {x}_{a}, \, {y}_{a}, \, {x}_{b}, \, {y}_{b} が1行で順に与えられる。

条件

  • 実行時間制限: 1s
  • メモリ制限: 256MB
  •  1 \leq t \leq {10}^{4}
  •  1 \leq {x}_{a}, \, {x}_{b} \leq h \leq {10}^{6}
    • すべてのテストケースにおける  h の値の合計は  {10}^{6} を超えない。
  •  1 \leq {y}_{a}, \, {y}_{b} \leq w \leq {10}^{9}
  •  ( {x}_{a}, \, {y}_{a} ) \ne ( {x}_{b}, \, {y}_{b} )

出力

各テストケースに対し、アリスが勝つ場合は Alice を、ボブが勝つ場合は Bob を、引き分けの場合は Draw を出力すること。

解法

まず、  {x}_{a} \geq {x}_{b} である場合には必ず引き分けとなります。 これは、アリスは  x 座標が1ターンで必ず  1 だけ大きくなっていくのに対して、ボブは  x 座標が1ターンで必ず  1 だけ小さくなっていくため、2人の駒が同じ  x 座標に位置することはないためです。

従って、  {x}_{a} \lt {x}_{b} の場合についてここから考えます。

まず、アリスとボブの駒が出会う可能性があるターン数について考えます。 このゲームでは、1ターンで  x 座標の差が  1 ずつ小さくなっていきます。

従って、初期状態では2人の駒の  x 座標の差が  {x}_{b} - {x}_{a} だけあるので、2人の駒が出会う可能性があるのは  ({x}_{b} - {x}_{a}) ターン目であるということが言えます。

このゲームではアリスが先攻となっていることから、奇数ターン目ではアリスの出番、偶数ターン目ではボブの出番ということになります。 よって、  ({x}_{b} - {x}_{a}) の値が奇数のときには「アリスが勝つ、もしくは引き分ける」という結果になり、偶数のときには「ボブが勝つ、もしくは引き分ける」という結果になるということが言えます。

ここで、まずは  ({x}_{b} - {x}_{a}) の値が奇数のとき、すなわちアリスが勝つ可能性があるときについて考えます。

まず、  {y}_{a} = {y}_{b} のときについては、必ずアリスが勝ちます。 これは2人とも  y 座標については  -1, \, 0, \, +1 の移動しかできないため、

  • 最初の1ターン目でアリスは  y 座標の値を変えずに移動する(マス  ({x}_{a} + 1, \, {y}_{a}) に移動する)
  • 次のターン以降は、ボブが移動した方向にアリスも移動する

という手順によって、常に  y 座標を一致させたまま  x 座標の値の差を縮めることができるためです。

次に、  {y}_{a} \lt {y}_{b} のときについて考えます。 このとき、初期状態ではアリスはボブよりも左側にいるため、アリスは右側に移動していくことによって、ボブを追い詰めていくことができます。

一方で、ボブも追いつかれてはならないため、右側に移動し続けることによって、追いつかれるまでのターン数を多く稼ぐことができます。

以上から、互いに  x 座標が一致するまで右方向に移動し続け、  w 列目に辿りついたら  y 座標を  w のままで移動し続ける、ということによって、アリスとボブが一致するか否かを判定することで、アリスが勝つかどうかの判定を行うことができます。

 {y}_{a} \gt {y}_{b} のときについても同様で、初期状態でアリスはボブよりも右側にいるため、アリスは左側に移動していくことによって、ボブを追い詰めていくことができます。

こちらの場合も、アリスとボブの  x 座標が一致するまでそれぞれの  y 座標を  1 ずつ減らしていくことによって、アリスとボブが一致するか否かを判定することができます。

以上のシミュレートは  O({x}_{b} - {x}_{a}) で行うことができます。

次に、  ({x}_{b} - {x}_{a}) が偶数のとき、すなわちボブが勝つ可能性がある場合について考えます。

まず、  {y}_{a} = {y}_{b} のときには必ずボブが勝ちます。 これは先程の場合と同様に、アリスが動いた方向にボブが動くことによって、必ず  y 座標を一致させながら  x 座標の差を縮めることができるためです。

次に、  {y}_{a} \lt {y}_{b} のときについて考えます。 このとき、アリスは先攻であり、初期状態ではアリスはボブよりも左側にいるため、アリスは左側に移動していくことによって、ボブから逃げていくことができます。

このようにアリスが逃げる際に、ボブも左側に移動していくことによってアリスを追い詰めることができるので、それぞれの  y 座標を  1 ずつ減らしていくことによって、アリスとボブが一致するか否かを判定することができます。

 {y}_{a} \gt {y}_{b} のときについても同様で、初期状態でアリスはボブよりも右側にいるため、アリスは右側に移動していくことによって、ボブから逃げていくことができます。

ボブも右側に移動していくことによってアリスを追い詰めることができるので、それぞれの  y 座標を  1 ずつ増やしていくことによって、アリスとボブが一致するか否かを判定することができます。

以上のシミュレートは、アリスが勝つ可能性がある場合と同様に  O({x}_{b} - {x}_{a}) で行うことができます。

よって、以上の方法によって、このゲームの決着がどのようになるかを判定することができます。

計算量については、  {x}_{b} - {x}_{a} \lt h であることから、  O(h) 程度で判定することができると言えます。 ここで、すべてのテストケースに対して  h の値の合計は  {10}^{6} を超えないことが条件で保証されているため、これは実行時間制限に十分間に合うと言えます。

ソースコード

まず、アリスとボブがともに左側に移動したときに、2つの駒が一致するかどうかを判定する関数 left_same() を以下のように実装しました。

int h, w, xa, ya, xb, yb;

bool left_same(int xa, int ya, int xb, int yb) {
  // 移動は xb - xa 回発生するので、そのそれぞれで ya, yb の値を更新していく
  for (int i = 1; i <= xb - xa; i++) {
   // ya と yb が一致すれば、必ず最終的には駒は一致するので true を返す
    if (ya == yb) {
      return true;
    }

    if (i % 2 == 1) {
      // i が奇数のときはアリスの駒が動く
      ya = max(1, ya - 1); // 1 よりも小さい値にならないことを注意して更新する
    } else {
      // i が偶数のときはボブの駒が動く
      yb = max(1, yb - 1); // 1 よりも小さい値にならないことを注意して更新する
    }
  }

  return ya == yb; // ya と yb が等しいかどうかを返す
}

この left_same() と同様に、アリスとボブがともに右側に移動したときに、2つの駒が一致するかどうかを判定する関数 right_same() を以下のように実装しました。

int h, w, xa, ya, xb, yb;

bool right_same(int xa, int ya, int xb, int yb) {
  // 移動は xb - xa 回発生するので、そのそれぞれで ya, yb の値を更新していく
  for (int i = 1; i <= xb - xa; i++) {
   // ya と yb が一致すれば、必ず最終的には駒は一致するので true を返す
    if (ya == yb) {
      return true;
    }

    if (i % 2 == 1) {
      // i が奇数のときはアリスの駒が動く
      ya = min(w, ya + 1); // w よりも大きい値にならないことを注意して更新する
    } else {
      // i が偶数のときはボブの駒が動く
      yb = min(w, yb + 1); // w よりも大きい値にならないことを注意して更新する
    }
  }

  return ya == yb; // ya と yb が等しいかどうかを返す
}

以上の2つの判定する関数がある状態で、最終的な結果を出力する関数 solve() を以下のように実装しました。

int h, w, xa, ya, xb, yb;

void solve() {
  // 値の入力
  cin >> h >> w >> xa >> ya >> xb >> yb;

  // xa >= xb のときは絶対に一致しないので、Draw を出力して終了する
  if (xa >= xb) {
    cout << "Draw" << endl;
    return;
  }

  if (abs(xa - xb) % 2 != 0) {
    // アリスが勝つ可能性がある場合
    if (ya <= yb && right_same(xa, ya, xb, yb)) {
      // ともに右に動く場合
      cout << "Alice" << endl;
    } else if (yb <= ya && left_same(xa, ya, xb, yb)) {
      // ともに左に動く場合
      cout << "Alice" << endl;
    } else {
      cout << "Draw" << endl;
    }
  } else {
    // ボブが勝つ可能性がある場合
    if (ya <= yb && left_same(xa, ya, xb, yb)) {
      // ともに右に動く場合
      cout << "Bob" << endl;
    } else if (yb <= ya && right_same(xa, ya, xb, yb)) {
      // ともに左に動く場合
      cout << "Bob" << endl;
    } else {
      cout << "Draw" << endl;
    }
  }
  return;
}

感想

 h の値の条件がなかったとしても、場合分けを細かく行なうことによって、このゲームの最終結果は求まるはずなので、「  h の値の合計が  {10}^{6} を超えない」という条件でこの問題が簡単になったように思います。 (ただ解いているだけの身としては、とてもありがたいです…。)

D - Very Different Array / Codeforces Round 920 (Div. 3)

問題

 n 個の整数からなる数列  a と、  m 個の整数からなる数列  b がある。(ただし、  n \geq m とする。)

数列  b から  n 個の要素を選び、新たに  n 個の整数からなる数列  c を作成する。 このとき数列  a, \, c に対して、  D = \displaystyle \sum_{i = 1}^{n} | {a}_{i} - {c}_{i} | と定めるとき、  D の最大値を求めよ。

入力

まず最初の1行目に、テストケースの個数を表す整数  t が与えられる。

その後、  t 個のテストケースのそれぞれに対して、以下のように入力が与えられる。

  • まず最初の1行目に、整数  n, \, m が与えられる。
  • 次の1行には、  n 個の整数  {a}_{1}, \, {a}_{2}, \, \cdots , \, {a}_{n} が順に与えられる。
  • 次の1行には、  m 個の整数  {b}_{1}, \, {b}_{2}, \, \cdots , \, {b}_{m} が順に与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 256MB
  •  1 \leq t \leq 100
  •  1 \leq n \leq  m \leq 2 \cdot {10}^{5}
    • すべてのテストケースにおける  m の値の合計は  2 \cdot {10}^{5} を超えない。
  •  1 \leq {a}_{i}, \, {b}_{i} \leq {10}^{9}

出力

各テストケースに対して、  D の最大値を1行で出力すること。

解法

一般性を失わないため、数列  a を昇順にソートされたものとして考えます。 すなわち、  {a}_{1} \leq {a}_{2} \leq \cdots \leq {a}_{n} として考えます。

ここで、 \begin{align} D & = \displaystyle \sum_{i = 1}^{n} | {a}_{i} - {c}_{i} | \\ & = | {a}_{1} - {c}_{1} | + | {a}_{2} - {c}_{2} | + \cdots + | {a}_{n} - {c}_{n} | \end{align} であるので、この値を最大化するには、

  • なるべく数列  a のうち、大きい値を正の値とする
  • なるべく数列  c のうち、大きい値を正の値とする

ということを同時に達成することが必要です。 従って、数列  c を降順に並び替えることによって、この値を最大化できると考えられます。 すなわち、  {c}_{1} \geq {c}_{2} \geq \cdots \geq {c}_{n} とすることによって最大化できます。

ここで、数列  b から  {a}_{i} 以上の要素を  i 個、  {a}_{i} 未満の要素を  (n - i) 個だけ取り出して数列  c を作成したと考えます。 このとき、  {c}_{1} \geq {c}_{2} \geq \cdots \geq {c}_{i} \geq {a}_{i} であり、  {a}_{i} \lt {c}_{i + 1} \leq \cdots \leq {c}_{n} が成立することから、 \begin{align} D & = | {a}_{1} - {c}_{1} | + | {a}_{2} - {c}_{2} | + \cdots + | {a}_{n} - {c}_{n} | \\ & = ({c}_{1} - {a}_{1}) + \cdots + ({c}_{i} - {a}_{i}) + ({a}_{i + 1} - {c}_{i + 1}) + \cdots + ({a}_{n} - {c}_{n}) \end{align} が成立します。

このことから、  {c}_{1}, \, {c}_{2}, \, \cdots , \, {c}_{i} については正の値を取り、  {c}_{i + 1}, \, {c}_{i + 2}, \, \cdots , \, {c}_{n} については負の値をとります。

従って、  D の値を最大化するためには、数列  b のうち大きい方から  i 個分のものを  {c}_{1}, \, \cdots , \, {c}_{i} とし、小さい方から  (n - i) 個分のものを  {c}_{n}, \, \cdots , \, {c}_{i + 1} とすることによって、達成することができます。

以上から、数列  b を降順に並び替えることを考えます。 すなわち、  {b}_{1} \geq {b}_{2} \geq \cdots \geq {b}_{m} とします。

このとき、数列  c [ {b}_{1}, \, \cdots , \, {b}_{i}, \, {b}_{i + m - n} , \, \cdots , \, {b}_{m} ] とすることによって、  D の値を最大化できます。

これを  0 \leq i \leq n であるすべての  i に対して数列  c を作り、それぞれの場合での  D の値を計算することによって得られる最大値が、この問題の答えとなります。

ただし、毎回数列  c を作成し  D を計算していくと、全体で  O( {n}^{2} ) の計算量がかかってしまい、これは実行時間制限には間に合いません。

そこで、値  \mathrm{sum 0}_{i}, \, \mathrm{sum 1}_{i} をそれぞれ、 \begin{align} & \mathrm{sum 0}_{i} = | {a}_{1} - {b}_{1} | + | {a}_{2} - {b}_{2} | + \cdots + | {a}_{i} - {b}_{i} | \\ & \mathrm{sum 1}_{i} = | {a}_{1} - {b}_{m - n + 1} | + | {a}_{2} - {b}_{m - n + 2} | + \cdots + | {a}_{i} - {b}_{m - n + i}| \end{align} として定めます。 また、  \mathrm{sum 0}_{0} = 0, \, \mathrm{sum 1}_{0} = 0 とします。

この値を整理すると、 \begin{align} & | {a}_{1} - {b}_{1} | + \cdots + | {a}_{i} - {b}_{i} | + | {a}_{i + 1} - {b}_{i + m - n}| + \cdots + | {a}_{n} - {b}_{m} | \\ = & \, \mathrm{sum 0}_{i} + ( | {a}_{1} - {b}_{m - n + 1} | + \cdots + | {a}_{n} - {b}_{m}|) - ( | {a}_{1} - {b}_{m - n + 1} | + \cdots + | {a}_{i} - {b}_{m - n + i} | ) \\ = & \, \mathrm{sum 0}_{i} + \mathrm{sum 1}_{n} - \mathrm{sum 1}_{i} \end{align} によって求めることができます。

従って、  \mathrm{sum 0}_{i}, \, \mathrm{sum 1}_{i} i = 1, \, 2, \, \cdots , \, n まで予め求めておくことにより、各  i に対して  D の値を  O(1) の計算量で求めることができます。 すなわち、全体で  O(n) の計算量で  D の値の最大値を求めることができます。

以上から、数列  a, \, b のソートも含めると、全体で  O(n \log n + m \log m) の計算量で  D の最大値を求めることができ、これは実行時間制限に十分間に合うと考えられます。

ソースコード

各テストケースに対して、  D の値の最大値を返す関数 solve() を以下のように実装しました。

int n, m;
ll a[int(2e5 + 5)], b[int(2e5 + 5)];
ll sum[int(2e5 + 5)][2];

ll solve() {
  // 値の入力
  cin >> n >> m;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  sort(a + 1, a + n + 1); // 数列 a の昇順ソート
  for (int i = 1; i <= m; i++) {
    cin >> b[i];
  }
  sort(b + 1, b + m + 1, greater<ll>()); // 数列 b の降順ソート

  // i = 1, ..., n の順に、上記の sum0, sum1 の値を計算していく
  for (int i = 1; i <= n; i++) {
    sum[i][0] = sum[i - 1][0] + abs(a[i] - b[i]); // sum0 の値を計算する
    sum[i][1] = sum[i - 1][1] + abs(a[i] - b[i + m - n]); // sum1 の値を計算する
  }

  ll ans = 0; // 最終的に答えとなる値 ans

  // i = 0, ..., n に対して D の値を計算していく
  for (int i = 0; i <= n; i++) {
    ans = max(ans, sum[i][0] + (sum[n][1] - sum[i][1])); // max を取り、 ans を更新していく
  }
  return ans;
}

感想

記事を書くにあたって、証明を行いづらい問題でした。