D - メッセージの伝言ゲーム / Telephone Game of Messages 解説
by
MMNMM
まず、生徒 \(1\) から伝言を開始したときの答えを求めることを考えます。
これは、\(1,T _ 1,T _ {T _ 1},T _ {T _ {T _ 1}},\ldots\) と計算していき、同じ値が出るまで続けることで求められます。
同じ値が出たかどうかは、はじめすべてを false で初期化した長さ \(N\) の配列を作っておき、出現した値に対応するところを true に書き換えるなどして判定できます。
真偽値ではなく「何番目で出現したか」を配列で管理しておくと、ループの長さを求めるのが簡単になります。
次のようなアルゴリズムで生徒 \(1\) に対する答えを求めることができます。
- 長さ \(N\) の列 \(\text{ord}\) を \(\text{ord}\leftarrow(0,0,\ldots,0)\) と初期化する。
- \(a\leftarrow i,j\leftarrow1\) と初期化して、以下を繰り返す。
- \(\text{ord}[a]\ne0\) なら、答えは \(j-\text{ord}[a]\) と求められる。繰り返しを終了する。
- \(\text{ord}[a]\leftarrow j\) と更新する。
- \(a\leftarrow T _ a\) と更新する。
この繰り返しは最大で \(N+1\) 回続き、すべての生徒に対してこれを行うと時間計算量は最悪で \(\Theta(N ^ 2)\) となってしまいます。
ここで、生徒 \(1\) から開始したときと生徒 \(T _ 1\) から開始したときの答えが変わらないことに着目してみます。 \(1,T _ 1,T _ {T _ 1},T _ {T _ {T _ 1}},\ldots\) のすべてに対して答えが変わらないので、生徒 \(1\) に対する答えが求められたら、生徒 \(T _ 1,\) 生徒 \(T _ {T _ 1},\ldots\) に対する答えを改めて計算する必要はありません。
よって、次のようなアルゴリズムを考えることができます。
- 長さ \(N\) の列 \(\text{ans}\) を \(\text{ans}\leftarrow(0,0,\ldots,0)\) と初期化し、長さ \(N\) の列 \(\text{ord}\) を \(\text{ord}\leftarrow(0,0,\ldots,0)\) と初期化する。
- \(i=1,2,\ldots,N\) に対して以下を繰り返す。
- \(a\leftarrow i,\text{used}\leftarrow(i),j\leftarrow1\) と初期化して、以下を繰り返す。
- \(\text{ans}[a]\ne0\) なら、列 \(\text{used}\) に含まれるすべての要素 \(u\) に対して、\(\text{ans}[u]\leftarrow\text{ans}[a]\) と更新し、繰り返しを終了する。
- \(\text{ord}[a]\ne0\) なら、列 \(\text{used}\) に含まれるすべての要素 \(u\) に対して、\(\text{ans}[u]\leftarrow j-\text{ord}[a]\) と更新し、繰り返しを終了する。
- \(\text{ord}[a]\leftarrow j\) と更新する。
- \(a\leftarrow T _ a,\text{used}\leftarrow\text{used}+{\!\!\!}+(T _ a),j\leftarrow j+1\) と更新する。
- \(a\leftarrow i,\text{used}\leftarrow(i),j\leftarrow1\) と初期化して、以下を繰り返す。
内側のループが \(K\) 回繰り返されたとき、\(K-1\) 人の生徒について新しく答えが求まっています。 よって、内側のループは合計で \(2N\) 回しか繰り返されず、全体の計算量も \(O(N)\) となります。
内側のループをグラフ上で DFS を行うように実装してもよいでしょう。 このように実装すると、明示的に配列 \(\text{used}\) を管理しなくても関数の再帰を利用して \(\text{used}\) に含まれるすべての要素に対して \(\text{ans}\) を更新できます。
実装例は以下のようになります。 C++ の実装例では DFS を使った方針を、Python の実装例では明示的に配列 \(\text{used}\) を管理する方針をとっています。
#include <iostream>
#include <vector>
using namespace std;
int main(){
int N;
cin >> N;
vector<int> T(N);
for (int& t : T) {
cin >> t;
--t; // 0-indexed にしておく
}
vector<int> ans(N), ord(N);
for (int i = 0; i < N; ++i) [&](this auto self, int now, int now_ord = 1) { // DFS を行う
if (ans[now]) { // すでに答えが求められているところに到達したら
return ans[now]; // その答えを返す
}
if (ord[now]) { // 今回計算したところに到達したら
ans[now] = now_ord - ord[now]; // ループの長さを計算
return ans[now];
}
ord[now] = now_ord; // 到達した順番を更新
ans[now] = self(T[now], now_ord + 1); // 答えを DFS で求める
return ans[now];
}(i);
for (int a : ans) {
cout << a << " ";
}
cout << endl;
return 0;
}
N = int(input())
T = [t - 1 for t in map(int, input().split())] # 0-indexed にしておく
ans = [0 for _ in range(N)]
ord = [0 for _ in range(N)]
for i in range(N):
now = i
used = [i]
now_ord = 1
while True:
if ans[now] > 0: # すでに答えが求められているところに到達したら
for u in used:
ans[u] = ans[now] # その答えで更新して
break # ループを抜ける
if ord[now] > 0: # 今回計算したところに到達したら
for u in used:
ans[u] = now_ord - ord[now] # ループの長さを計算して
break # ループを抜ける
ord[now] = now_ord # 到達した順番を更新
# 一人進める
now = T[now]
used.append(now)
now_ord += 1
print(*ans)
投稿日時:
最終更新:
