Official

D - Bank Editorial by Nyaan


2 つの解法を紹介します。

解法 1 (std::set を使う解法)

1 つ目は std::set のような順序付き集合型を利用する方法です。 具体的には、次の手順で「受付に呼ばれたが、まだ受付に行っていない人」の集合を管理しながらイベントを処理していきます。

  • called を 「受付に呼ばれたが、まだ受付に行っていない人」の集合とする。はじめ called は空である。
  • また、last を最後に呼ばれた人の番号とする。便宜上 はじめ last00 とする。
  • その後、イベントを順に処理する。イベントの種類に応じて次の処理を行う。
    • 1 種類目のイベントの場合、次に呼ばれるのは last + 1 である。よって last に 1 を足したあとに calledlast を追加する。
    • 2 種類目のイベントの場合、xcalled から削除する。
    • 3 種類目のイベントの場合、called の最小の要素が答えである。よってこれを出力する。

上記の手順中、called に関する操作は「要素の追加」「要素の削除」「最小の要素の出力」です。called の実装に配列や list などを用いると、最悪ケースで時間計算量が O(N2)\mathrm{O}(N^2) かかり TLE してしまいますが、std::set のような順序付き集合型を用いることで操作をすべて 1 回あたり O(logN)\mathrm{O}(\log N) で行うことができるので、クエリ全体で O(QlogN)\mathrm{O}(Q \log N) でこの問題を解くことができます。

  • 実装例 (C++, O(QlogN)\mathrm{O}(Q \log N) )
Copy
  1. #include <iomanip>
  2. #include <iostream>
  3. #include <set>
  4. #include <vector>
  5. using namespace std;
  6. int main() {
  7. ios::sync_with_stdio(false);
  8. cin.tie(nullptr);
  9. int N, Q;
  10. cin >> N >> Q;
  11. set<int> called;
  12. int last = 0;
  13. while (Q--) {
  14. int event;
  15. cin >> event;
  16. if (event == 1) {
  17. called.insert(++last);
  18. } else if (event == 2) {
  19. int x;
  20. cin >> x;
  21. called.erase(x);
  22. } else {
  23. cout << *begin(called) << "\n";
  24. }
  25. }
  26. }
#include <iomanip>
#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int N, Q;
  cin >> N >> Q;

  set<int> called;
  int last = 0;
  while (Q--) {
    int event;
    cin >> event;
    if (event == 1) {
      called.insert(++last);
    } else if (event == 2) {
      int x;
      cin >> x;
      called.erase(x);
    } else {
      cout << *begin(called) << "\n";
    }
  }
}

解法 2 (線形時間で解く方法)

より計算量の良い解法として、時間計算量が O(N+Q)\mathrm{O}(N + Q) である解法も紹介します。

問題の制約から、3 種類目のイベントで呼ばれる番号は広義単調増加するという性質を持ちます。

  • 証明:背理法で考えます。3 種類目のイベントで人 yy が呼ばれて、その後 人 xx (x<y)(x < y) が呼ばれたとします。すると、yy が呼ばれた時点で、人 xx はすでに 1 回以上受付に呼ばれていて(1 回目の呼び出しは番号順であるため) かつ まだ受付に行っていないため、矛盾が発生します。

よって、次のような手順で線形時間で解くアルゴリズムを構成できます。まず、最後に 3 種類目のイベントで読んだ人の番号を変数 ans で常に保持しておきます。そして、3 種類目のイベントが来るたびに、ans の示す人が受付に行っていない人になるまで ans の値をインクリメントして、得られる ans の値を出力します。
この操作によって ans のインクリメントされる回数は min(N,Q)\min(N, Q) 回で抑えられるので、計算量は O(N+Q)\mathrm{O}(N+ Q) になります。

  • 実装例 (C++, O(N+Q)\mathrm{O}(N + Q))
Copy
  1. #include <iomanip>
  2. #include <iostream>
  3. #include <vector>
  4. using namespace std;
  5. int main() {
  6. ios::sync_with_stdio(false);
  7. cin.tie(nullptr);
  8. int N, Q;
  9. cin >> N >> Q;
  10. vector<int> gone(N + 1);
  11. int ans = 1;
  12. while (Q--) {
  13. int event;
  14. cin >> event;
  15. if (event == 1) {
  16. // do nothing
  17. } else if (event == 2) {
  18. int x;
  19. cin >> x;
  20. gone[x] = 1;
  21. } else {
  22. while (gone[ans]) ans++;
  23. cout << ans << "\n";
  24. }
  25. }
  26. }
#include <iomanip>
#include <iostream>
#include <vector>
using namespace std;

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int N, Q;
  cin >> N >> Q;
  vector<int> gone(N + 1);
  int ans = 1;
  while (Q--) {
    int event;
    cin >> event;
    if (event == 1) {
      // do nothing
    } else if (event == 2) {
      int x;
      cin >> x;
      gone[x] = 1;
    } else {
      while (gone[ans]) ans++;
      cout << ans << "\n";
    }
  }
}

posted:
last update:



2025-04-05 (Sat)
10:36:43 +00:00