D - メッセージの伝言ゲーム / Telephone Game of Messages 解説 by admin
Gemini 3.0 Flash (Thinking)概要
この問題は、各頂点からちょうど1本の有向辺が出ているグラフ(機能グラフ / Functional Graph)において、各頂点から出発してたどり着く「ループ(閉路)」の長さを求める問題です。
考察
各生徒が次にメッセージを渡す相手は一人だけなので、どの生徒から開始しても、いつかは必ずループに到達します。
素朴なアプローチとその限界
すべての生徒 \(i\) について、実際に一人ずつシミュレーションを行う方法が考えられます。しかし、この方法では最悪の場合、1回の探索に \(O(N)\) かかり、全体で \(O(N^2)\) の時間がかかってしまいます。\(N = 2 \times 10^5\) という制約下では、実行時間制限(TLE)に間に合いません。
効率的な解法へのヒント
このグラフの構造には以下の特徴があります: 1. 必ず一つのループに到達する: どの頂点からスタートしても、最終的には一つのループに入り、そこから抜け出すことはありません。 2. 同じパス上の頂点は同じ結果になる: ある生徒 \(A\) から辿った先にループがあるとき、その途中にいる生徒 \(B\) からスタートしても、最終的には同じループに到達します。したがって、一度計算した結果は再利用できます。
この特徴を利用し、一度訪れた頂点の結果を記録(メモ化)しながら探索することで、各頂点を高々数回調べるだけで済むようになります。
アルゴリズム
各頂点の状態を以下の3つの状態で管理しながら、全頂点を走査します。
- 未訪問 (0): まだ一度も探索されていない。
- 探索中 (1): 現在のスタート地点からの探索パスに含まれている。
- 探索完了 (2): 既にどのループに到達するかが判明している。
具体的な手順
- まだ「未訪問」の生徒 \(i\) を選んで探索を開始します。
- 「未訪問」の生徒を辿っている間、その生徒を「探索中」にし、パス(訪れた順序)を記録しておきます。
- 次に進む相手が以下のいずれかの状態になるまで繰り返します:
- 「探索中」の生徒にぶつかった場合:
新しいループを発見したことを意味します。その生徒がパスの何番目に登場したかを確認することで、ループの長さ(
現在のパスの長さ - 登場位置)を計算できます。 - 「探索完了」の生徒にぶつかった場合: 既に解析済みの構造に合流したことを意味します。その生徒が最終的に到達するループの長さは既に分かっているので、それをそのまま利用します。
- 「探索中」の生徒にぶつかった場合:
新しいループを発見したことを意味します。その生徒がパスの何番目に登場したかを確認することで、ループの長さ(
- 探索したパス上のすべての生徒を「探索完了」にし、求めたループの人数を記録します。
これを全生徒に対して行うことで、すべての生徒について効率的に答えを求めることができます。
計算量
- 時間計算量: \(O(N)\) 各頂点と各辺をたどる回数は定数回(「未訪問」から「探索中」、「探索中」から「探索完了」への遷移のみ)であるため、生徒数 \(N\) に対して線形時間で処理が完了します。
- 空間計算量: \(O(N)\) 渡し先の配列、訪問状態の配列、結果の配列、および探索時のパスを保持するために \(O(N)\) のメモリを使用します。
実装のポイント
再帰の回避: Pythonで深い探索を行う場合、再帰関数を使うとスタックオーバーフロー(
RecursionError)が発生する可能性があるため、whileループを用いた反復的な実装が安全です。0-indexed への変換: 入力の生徒番号は \(1\) から \(N\) ですが、プログラム内では \(0\) から \(N-1\) として扱うと、配列のインデックス操作がスムーズになります。
パス内の位置記録: ループの長さを一瞬で計算するために、現在の探索パスにおいて「どの生徒が何番目に出現したか」を辞書(
path_map)などで記録しておくと効率的です。ソースコード
import sys
def solve():
# 入力を一度にすべて読み込みます
data = sys.stdin.read().split()
if not data:
return
# 生徒の数 N
n = int(data[0])
# 各生徒 i のメッセージの渡し先 T_i
# 0-indexed に変換して管理します(生徒 1 はインデックス 0、生徒 2 はインデックス 1 ...)
t = [int(x) - 1 for x in data[1:]]
# 各生徒から開始したときの伝言ループメンバーの人数を格納する配列
ans = [0] * n
# 訪問状態を管理する配列
# 0: 未訪問, 1: 現在の探索パスに含まれている, 2: 探索完了済み(確定済み)
visited = [0] * n
for i in range(n):
# 既に探索済みの生徒はスキップ
if visited[i] == 2:
continue
curr = i
path = []
# 現在の探索パス内での生徒の出現位置を記録する辞書
path_map = {}
# メッセージの伝言をたどる
while visited[curr] == 0:
path_map[curr] = len(path)
path.append(curr)
visited[curr] = 1
curr = t[curr]
# 停止したとき、curr は「過去に既にメッセージを受け取ったことのある生徒」
if visited[curr] == 1:
# 現在の探索パス内で既に訪れた生徒に到達した場合(新しいループを発見)
# ループの開始位置を取得
cycle_start_idx = path_map[curr]
# ループに含まれる生徒の人数を計算
cycle_len = len(path) - cycle_start_idx
# この探索パス上のすべての生徒は、最終的にこのループに到達する
for node in path:
ans[node] = cycle_len
else:
# 既に探索完了済みの生徒に到達した場合(既存の構造に合流)
# その生徒が最終的に到達するループの人数をそのまま利用
cycle_len = ans[curr]
for node in path:
ans[node] = cycle_len
# 今回たどったパス上のすべての生徒を探索完了済み(visited = 2)にする
for node in path:
visited[node] = 2
# 結果をスペース区切りで出力
print(*(ans))
if __name__ == '__main__':
solve()
この解説は gemini-3-flash-thinking によって生成されました。
投稿日時:
最終更新: