
配点: 400 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: ??? / NoviSteps: 2Q
問題概要
マス目があり、上から
行目、左から
列目のマスを
と表す。
マス目の上には 個のゴミが落ちており、
番目のゴミはマス
に落ちている。
個のクエリが与えられるので、順に処理せよ。各クエリは以下の2形式のいずれかである。
1 x
: マス目の行目に落ちているゴミの個数を出力する。その後、
行目に落ちているゴミはすべて捨てられ、マス目上から取り除かれる。
2 y
: マス目の列目に落ちているゴミの個数を出力する。その後、
列目に落ちているゴミはすべて捨てられ、マス目上から取り除かれる。
制約
- ゴミの位置は相異なる
- 入力される値はすべて整数
考察
クエリは行および列を指定するものなので、各行・列ごとに存在するゴミの個数を管理できるようにする。これは、vector
の2次元配列で実現できる。
要素としては、当該行・列に存在するゴミの番号とする。別途長さ のフラグ配列を用意することで、各ゴミが捨てられたかどうかを判定できるようにするためだ。
set
を使えばinsert
/ delete
で実現できるが、今回はvector
なのでそれはできない。
ここまでの内容を基にすると、以下のようなアルゴリズムが考えられる。
- 各行・列ごとに存在するゴミの個数を管理する二次元配列
rows
,cols
に、それぞれゴミの番号を格納する。 - 各ゴミが捨てられたかどうかを管理する配列
is_removed
を用意し、false
で初期化する。 - 各クエリについて、以下を行う(列についても同様)。
- 指定された行
の各要素について、
is_removed[x]
がfalse
であるならば、- 答えのカウンターを
増やす。
is_removed[x]
をtrue
とする。
- 答えのカウンターを
- カウンターを出力する。
- 指定された行
しかし、このアルゴリズムでは計算量が となり、 TLE してしまう。
実は、一度操作が行われた行・列はゴミが存在しなくなるので、上述のアルゴリズムの手順 3-1. は高々1回行えばよいことになる。
したがって、新たに操作が行われたかどうかを管理するフラグ配列を用意し、手順 3-1. でその真偽を判定すればよい。詳しくは実装例参照。
これによって、クエリ処理部分の計算量は となり改善することができた。
実装例
#include <bits/stdc++.h> using namespace std; #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) // ======================================== // int main() { int H, W, N; cin >> H >> W >> N; vector<vector<int>> rows(H), cols(W); rep(i, 0, N) { int X, Y; cin >> X >> Y; X--, Y--; rows[X].push_back(i); cols[Y].push_back(i); } int Q; cin >> Q; vector<bool> is_removed(N, false); vector<bool> is_removed_rows(H, false), is_removed_cols(W, false); while (Q--) { int t; cin >> t; int cnt = 0; if (t == 1) { int x; cin >> x; x--; if (!is_removed_rows[x]) { for (auto &&i : rows[x]) { if (!is_removed[i]) { cnt++; is_removed[i] = true; } } is_removed_rows[x] = true; } cout << cnt << endl; } else { int y; cin >> y; y--; if (!is_removed_cols[y]) { for (auto &&i : cols[y]) { if (!is_removed[i]) { cnt++; is_removed[i] = true; } } is_removed_cols[y] = true; } cout << cnt << endl; } } return 0; }
実装時間: 20分
コメント
「行と列に分けてデータを管理する」というのは、クエリ処理系の典型の考え方だと思う。