I - 円陣パスゲーム / Circle Pass Game 解説 by admin
Claude 4.6 Opus (Thinking)概要
円形に並んだ \(N\) 人の子供が順にボールをパスし、パスした子供は抜けていくシミュレーション問題。BIT(Binary Indexed Tree)を用いて、残っている子供の中から \(k\) 番目を高速に求める。
考察
素朴なアプローチの問題点
単純にリストや配列で円陣を管理し、1人ずつ数えていく方法では、\(D_i\) が最大 \(10^9\) と非常に大きいため、1つずつカウントすると TLE になります。
重要な気づき
剰余による高速化: 残っている子供が \(R\) 人(ボール保持者を除く)のとき、\(D_i\) 人目を数えるのは円を何周もする可能性がありますが、実質的には \((D_i - 1) \mod R + 1\) 番目と同じです。これで数える人数を \(R\) 以下に抑えられます。
「\(k\) 番目に残っている子供を見つける」問題: 円陣から抜けた子供を飛ばして数える操作は、「現在残っている子供の中で、ある位置から時計回りに \(k\) 番目の子供は誰か?」という問題に帰着できます。
BIT で \(k\) 番目を求める: BIT に各子供の存在(1 or 0)を格納すると、累積和で「位置 \(i\) 以下に何人残っているか」が分かります。さらに、BIT 上の二分探索(
find_kth)で「全体で \(k\) 番目に残っている子供の番号」を \(O(\log N)\) で求められます。
アルゴリズム
- BIT を初期化し、全位置に 1 をセット(全員が円陣に参加)。
- 各パスについて以下を行う:
- 残り人数 \(R = N - i - 1\)(現在のボール保持者を除く)で \(D_i\) の剰余を取り、実効的な距離
effective_dを計算。 - 現在の位置
currentより後ろ(番号が大きい側)に残っている子供の人数afterを求める。 effective_d <= afterなら、currentより後ろの中でeffective_d番目を探す。具体的には、全体での累積順位bit.query(current) + effective_d番目の子供をfind_kthで求める。- そうでなければ、先頭に戻って
effective_d - after番目の子供を探す。
- 残り人数 \(R = N - i - 1\)(現在のボール保持者を除く)で \(D_i\) の剰余を取り、実効的な距離
currentを BIT から削除(値を -1 に更新)し、ボール保持者を更新。- \(M\) 回終了後、
currentを出力。
具体例
\(N=5, S=3, D_1=7\) の場合:
- 残り人数 \(R=4\)、effective_d \(= (7-1) \mod 4 + 1 = 3\)
- 位置 3 から時計回りに 3 人目 → 4, 5, 1 と数えて 1 にパス
- 位置 3 を削除
計算量
- 時間計算量: \(O((N + M) \log N)\)(初期化に \(O(N \log N)\)、各パスに \(O(\log N)\))
- 空間計算量: \(O(N)\)
実装のポイント
find_kthの実装: BIT 上で二分探索を行い、累積和が \(k\) 以上になる最小の位置を \(O(\log N)\) で求める。通常の二分探索 +queryだと \(O(\log^2 N)\) になるが、BIT の内部構造を利用することで \(O(\log N)\) に削減。円環の処理: 「current より後ろ」と「先頭から」の 2 つのケースに分けることで、円環上の巡回を線形配列上の操作に変換している。
\(D_i\) のオーバーフロー: \(D_i\) が最大 \(10^9\) なので
long longで受け取り、剰余計算を正しく行う。ソースコード
#include <bits/stdc++.h>
using namespace std;
struct BIT {
int n, log_n;
vector<int> tree;
BIT(int n) : n(n), tree(n + 1, 0) {
log_n = 0;
while ((1 << (log_n + 1)) <= n) log_n++;
}
void update(int i, int val) {
for (; i <= n; i += i & (-i))
tree[i] += val;
}
int query(int i) {
int s = 0;
for (; i > 0; i -= i & (-i))
s += tree[i];
return s;
}
int find_kth(int k) {
int pos = 0;
for (int pw = (1 << log_n); pw > 0; pw >>= 1) {
if (pos + pw <= n && tree[pos + pw] < k) {
pos += pw;
k -= tree[pos];
}
}
return pos + 1;
}
};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N, M, S;
cin >> N >> M >> S;
BIT bit(N);
for (int i = 1; i <= N; i++)
bit.update(i, 1);
int current = S;
for (int i = 0; i < M; i++) {
long long D;
cin >> D;
long long remaining = N - i - 1; // countable children (excluding current holder)
int effective_d = (int)(((D - 1) % remaining) + 1);
// Count active children with index > current
int after = bit.query(N) - bit.query(current);
int target;
if (effective_d <= after) {
int rank = bit.query(current) + effective_d;
target = bit.find_kth(rank);
} else {
int k = effective_d - after;
target = bit.find_kth(k);
}
// Remove current from circle
bit.update(current, -1);
current = target;
}
cout << current << endl;
return 0;
}
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: