D – Teleporter 解説 (AtCoder Beginner Contest 167)
問題概要
町が \(N\) 個ある。町 \(i\) から町 \(A_i\) に移動することを K 回繰り返す。
町 1 から始めた時、最終的にどの町にたどり着くか?
制約
- \(2 \leq N \leq 2 \times 10^5\)
- \(1 \leq A_i \leq N\)
- \(1 \leq K \leq 10^{18}\)
考え方
単純にシミュレーションをしようとしても、\(K\) が大きいので計算が間に合いません。
簡単な例(例えば入力例2)などで試してみると様子がつかめます。
ある程度進んで、今までに到達したことのある町に到達したら、以降は周期的に同じ場所をグルグル回ることになります。
例えば、Aが
- 6 5 2 5 3 2
の時、どのような移動の仕方になるかというと、
- 1→6→(2→5→3)→(2→5→3)→(2→5→3)→(2→5→3)→…
というようになり、始めの「1→6」が終わったら、以降は「2→5→3」を繰り返すことになります。繰り返しの部分をシミュレーションする必要はなく、繰り返し部分の長さ(ここでは3)での余りを考えることで、必要な計算時間を大幅に減らすことができます。
もう一つの解法としては、ダブリングという概念を利用した方法もあります。
ダブリングは全体の要素数がN個のとき「K個先の要素を求めるのに \(O(K)\) かかる」ような状況を
- 前処理:\(O(N \log K)\) 時間, \(O(N \log K)\) 空間
- クエリ:\(O(\log K)\)
で行うことができるようにするアルゴリズムです。
解法1: 周期性と剰余を利用した方法
まずは同じ町が出現するまでシミュレーションをして、到達した順番を保存しておきます。少なくともN回移動すれば、今までに到達したことのある町に再び訪れることになります(鳩の巣原理)。よって、計算量は O(N) になります。
周期性のある場所に到達するまでを s、周期が r とすると、
- K < s なら、K番目に到達する町を出力
- s <= K なら、(K-s)%r + s 番目に到達する町を出力
C++ での実装例
問題文は 1-indexed ですが、C++ は 0-indexed なので、微妙にずらしてやる必要があります。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
    ll N, K;
    cin >> N >> K;
    vector<int> A(N);
    for (int i = 0; i < N; i++) {
        cin >> A.at(i);
        A[i]--;  // 0-indexed に変更
    }
    set<int> used;     // 探索済みの町を保存
    vector<int> path;  // 探索の順番を格納
    int now = 0;
    while (used.count(now) == 0) {  // 探索済みの町に到達するまで
        path.push_back(now);
        used.insert(now);
        now = A[now];
    }
    int s = find(path.begin(), path.end(), now) - path.begin();  // s: ループする最初の部分に到達するまで
    int r = (int)path.size() - s;                                // 周期
    if (K < s) {  // ループ部分に到達しない時
        cout << path[K] + 1 << endl;
        return 0;
    } else {  // ループ部分に到達する時
        cout << path[(K - s) % r + s] + 1 << endl;
        return 0;
    }
}
解法2: ダブリングを利用した方法
ダブリングを用いることでも解くことができます。
ここでは、
- doubling[k][i] : 町 \(i\) から \(2^k\) 先の町はどこか?
という情報を前計算し、繰り返し二乗法のような2進数で見たときのビットに注目した計算をすることで、K個先の要素を簡単に取得することができます。(そもそも繰り返し二乗法がダブリングの1種とも言えます。)
- 前処理:\(O(N \log K)\) 時間, \(O(N \log K)\) 空間
- クエリ:\(O(\log K)\)
となるので、十分高速です。
C++ での実装例
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
    ll N, K;
    cin >> N >> K;
    vector<int> A(N);
    for (int i = 0; i < N; i++) {
        cin >> A.at(i);
        A[i]--;  // 0-indexed に変更
    }
    int logK = 1;
    while ((1LL << logK) <= K) logK++;
    // doubling[k][i] : i番目から 2^k 進んだ町
    vector<vector<int> > doubling(logK, vector<int>(N));
    for (int i = 0; i < N; i++) {
        doubling[0][i] = A[i];
    }
    // 前処理 doubling の計算
    for (int k = 0; k < logK - 1; k++) {
        for (int i = 0; i < N; i++) {
            doubling[k + 1][i] = doubling[k][doubling[k][i]];
        }
    }
    int now = 0;
    for (int k = 0; K > 0; k++) {
        if (K & 1) now = doubling[k][now];
        K = K >> 1;
    }
    cout << now + 1 << endl;
}
ディスカッション
コメント一覧
まだ、コメントがありません