先日、2026/6/13(土)に開催されたABC462(AtCoder Beginner Contest 462)のD問題を自分なりに整理し、図とともにC++コードを掲載します。
D - Accomplice
殺人事件の犯人候補から、容疑者と犯行時刻を絞り込み、組み合わせの数を考える問題です。
犯行場所は館で、犯人は二人で犯行には単位時間かかったということがわかっています。犯人二人は
単位時間の間同時に館にいる必要があります。
人の犯人候補が館に入った時刻
と館から出た時刻
はわかっています。
この条件から犯行時刻と犯人の組み合わせの数を求める問題です。
まず犯人となる候補の組み合わせを考えようとしました。
番目の人が犯人と仮定した場合、
から
の間に誰と共犯になり得るかを考えればいいのではという考えです。
愚直に実装するなら
番目の人を基準に
となる
番目の共犯者と犯行時刻
から
の間で
単位時間を取れるかどうかの組み合わせとなりそうです。
犯人の組み合わせを考えるだけで、
、さらに時刻の組み合わせまで考えると大変なことになりそうです。
そこで考え方を変えて、犯行開始時刻を基準に考えてみました。
犯行開始時刻を基準に考える場合、犯行開始時刻は整数なので最大でも
通り以下となります。各犯行時刻に対して少ない計算量で犯人候補の組み合わせ数を求められたらOKそうです。
犯行開始時刻で犯人になり得る人というのは、言い換えると、時刻
から時刻
まで館にいる人、さらに言い換えると、時刻
以前に館に入り、時刻
以降に館を出た人になります。
この条件を満たす犯人候補が2人以上いたら犯行可能な時間ということになります。その時の犯人の組み合わせは、可能な人が
人いた場合
で計算できます。
つまり、時刻に犯行開始できた人の組み合わせは、時刻
以前に館に入り、時刻
以降に館を出た人の人数がわかれば計算できます。
時刻ごとに人の出入りをシミュレーションすれば良いのです。
- 時刻
になった時に入る人がいれば追加する。ただし、時刻
より前に出る人はそもそも犯行不可能なので追加しない
- 館にいる人を数える
に出ていく人がいれば人数を減らす
私はこれを優先度キュー(C++ではpriority_queue)を二つの待ち行列として実装しました。
- 待ち行列1:まだ館に入っていない人を入る時刻で昇順にソートした待ち行列
- 待ち行列2:館にいる人を出る時刻で昇順ソートした待ち行列
入力例1でシミュレーションの動作を見てみます。です。
まず、3人を館に入る時刻で昇順ソートした待ち行列1を作ります。
各時刻で待ち行列1の先頭の人の
が時刻
と一致するかどうかを確認します。
先頭の人が時刻
に館を出ない場合は先頭だけを確認すれば良いので、
で確認することができます。
最初の人が館に入るのは時刻9なので、時刻8までこの状態が続きます。
時刻9の時点で初めて先頭の人が館に入ることになります。
そこで待ち行列1から先頭の人を取り出します。。
その人を館の中に入れ、出る時刻を基準とした待ち行列2に追加します。
この時点で時刻9から時刻11(=9+2)まで館の中にいる人は一人なので、犯行は不可能です。
次に時刻10の時点を考えます。
待ち行列1にS=10の人がいるため館の中に入ります。この人は出る時刻は12(10+2)以降なので、一旦館の中に入れます。
この人は出る時刻が12で元々いる人より早いため待ち行列2の先頭に挿入します。。
この時点で時刻10から時刻12(=10+2)の間にいた人は2人になるので、犯行が可能となり、組み合わせの合計を一つ増やします。
組み合わせを増やした後にに館を出た人を館から出します。
つまり、12(=10+2)に出た人として、入ったばかりですが、先頭の人を館から出します。
ここでの実装は待ち行列2の先頭の人の出る時刻が
に一致するかどうかを確認します。
。
これで時刻10の処理は完了です。
次に時刻13では待ち行列1の先頭の人のが13で、かつ出る時刻
が15(=13+2)より大きいので館の中に入れます。この人は出る時刻
が元いる人より大きいため、待ち行列2の最後尾に入れます。
この時点で時刻13から15の間にいた人は2人なので、犯行可能な組み合わせを1増やします。
同様に時刻14,15もその時刻から+2した時刻までは館内に2人いたことになるので、それぞれ犯行可能な組み合わせを1増やし、合計4になります。
時刻15で犯行可能な組み合わせを数えたのちに、時刻17(=15+2)で館を出る人がいるため、待ち行列2から先頭の人を館の外に出します。 以降は、館の中には1人以下しかいないため、これ以上犯行可能な組み合わせはありません。
以上のシミュレーションで、犯行可能な組み合わせは4と求まりました。
この入力例では館の中に同時に2人までしかいませんでしたが、館の中に人いた場合は
で組み合わせを増やすようにすれば、一般化できます。
それぞれの処理の計算量は大きく見積もって以下になります。
- 待ち行列1を作成
- 各時刻で待ち行列1の先頭の人の入る時間を確認
- 各時刻で待ち行列1から先頭の人を取り出す
- 各時刻で待ち行列2に追加
- 各時刻で待ち行列2の先頭の人の出る時間を確認
- 各時刻で待ち行列2から先頭の人を取り出す
- 各時刻で人数を数え、組み合わせを計算
最大でも (
)なので十分間に合います。
以下が実装です。
using ll = long long; int main() { int N, D; cin >> N >> D; // 館に入る待ち行列 priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> Q1; for (int i = 0; i < N; i++) { int S, T; cin >> S >> T; Q1.push({S, T}); } // 館から出る待ち行列 priority_queue<int, vector<int>, greater<int>> Q2; ll count = 0; for (int x = 0; x <= 1000000; x++) { // その時刻に館に入る人を館に入れる while (!Q1.empty() && Q1.top().first == x) { if (Q1.top().second >= x + D) Q2.push(Q1.top().second); Q1.pop(); } // 犯行可能な組み合わせを数えて加算 if (Q2.size() >= 2) { ll n = Q2.size(); count += n * (n - 1) / 2; } // 組み合わせを数えてからx+Dに館から出る人を館から出す while (!Q2.empty() && Q2.top() == x + D) { Q2.pop(); } } cout << count << endl; }