Official

B - Yellow and Red Card Editorial by Nyaan


この問題はいくつかの方針が考えられる問題です。制約が十分小さいため、基本的には問題文のルールを理解した上で実装すればどのような解法でも AC することができると思います。
例えば、シンプルなアルゴリズムとして次の手順が考えられます。

  • 過去のイベントを保存するリスト \(v\) を用意する。\(v\) ははじめ空である。
  • \(i = 1, 2, \dots, N\) の順に次の操作を行う。
    • \(i\) 番目のイベント \((c_i, x_i)\) を受け取る。\(c_i\) の値によって場合分けを行う。
      • \(c_i=1,2\) の場合何もしない。
      • \(c_i = 3\) の場合、\(i-1\) 番目までのイベントを \(v\) を走査して全て調べて、人 \(x_i\) に出されたイエローカード・レッドカードの枚数をカウントする。そして、カードの枚数が退場処分を受ける条件を満たしていれば Yes を、そうでなければ No を出力する。
    • その後、\((c_i, x_i)\)\(v\) に追加する。

このアルゴリズムは、\(i\) 回目のクエリの時に \(i-1\) 個のイベントを走査するため、走査の部分のステップ数は最大で \(\sum_{i=1}^Q (i-1) = \frac{Q^2-Q}{2}\) ステップになります。よって計算量は \(\mathrm{O}(Q^2)\) です。

上記のアルゴリズムでも AC するのに十分高速ですが、もう少し工夫すると計算量を \(\mathrm{O}(N + Q)\) に改善することができます。
計算量を改善する方法として、それぞれの人について「イエローカードを何枚持っているか?」「退場処分を受けたか?」をメモする配列を用意して、イベントが起こるたびに配列を更新する方針が考えられます。これによって質問のたびに過去のイベントをすべて調べる必要が無くなり高速化することができます。
具体的には、次の手順を実装すればよいです。

  • 「人 \(x\) はイエローカードを何枚もらったか?」を意味する長さ \(N\) の配列 \(y\) と「人 \(x\) は退場処分を受けたか?」を意味する長さ \(N\) の配列 \(r\) をあらかじめ用意する。\(y\)\(0\) で、\(r\) は false で初期化されている。
  • \(i = 1, 2, \dots, N\) の順に次の操作を行う。
    • \(i\) 番目のイベント \((c_i, x_i)\) を受け取る。\(c_i\) の値によって場合分けを行う。
      • \(c_i = 1\) の場合、\(y[x]\)\(1\) を加算する。そして加算したあとの \(y[x]\)\(2\) だった場合、人 \(x\) は退場処分の条件を満たすので \(r[x]\) を true にする。
      • \(c_i = 2\) の場合、人 \(x\) は退場処分の条件を満たすので \(r[x]\) を true にする。
      • \(c_i = 3\) の場合、\(r[x]\) の値が true ならば Yes , falseならば No を出力する。

計算量は \(\mathrm{O}(N + Q)\) です。この方針だと\(N, Q \leq 10^5\) でも高速に動くプログラムを作ることができます。

  • 実装例(C++)
#include <iostream>
#include <vector>
using namespace std;
int main() {
  int N, Q;
  cin >> N >> Q;
  vector<int> r(N + 1), y(N + 1);
  while (Q--) {
    int cmd, x;
    cin >> cmd >> x;
    if (cmd == 1) {
      y[x]++;
      if (y[x] == 2) r[x] = 1;
    } else if (cmd == 2) {
      r[x] = 1;
    } else {
      cout << (r[x] ? "Yes" : "No") << endl;
    }
  }
}

Bonus (C 問題相当) : \(N = 10^{18}, Q = 10^5\) の場合も高速に動作する方針を考えてみましょう。

posted:
last update: