Day 079-Q1 — Functional Graph: Cycle Detection + Binary Lifting(ρ形状・周期クエリ)

2026-07-01 赤色 Master / Phase 8+ ★★★★★★★★★ Functional Graph・ρ形状・Binary Lifting・周期クエリ

問題

$N$ 頂点の機能グラフ(Functional Graph)が与えられる。 各頂点 $i$($1 \le i \le N$)にはちょうど 1 本の有向辺が出ており、その先は $f_i$ である。 つまり、各頂点から辺を 1 回たどると頂点 $f_i$ に移動する。

$Q$ 個のクエリが与えられる。各クエリ $(v, K)$ に対して、 「頂点 $v$ から $K$ 回移動したとき、最終的にいる頂点番号」を答えよ。

機能グラフの各弱連結成分はちょうど 1 つのサイクル(自己ループも含む)を持つ($\rho$ 形状)。 $K$ は最大 $10^{18}$ と非常に大きいため、シミュレーションでは絶対に間に合わない。

入力形式

N Q
f_1 f_2 ... f_N
v_1 K_1
v_2 K_2
...
v_Q K_Q

制約

パラメータ範囲備考
$N$$1 \le N \le 2 \times 10^5$頂点数
$Q$$1 \le Q \le 2 \times 10^5$クエリ数
$f_i$$1 \le f_i \le N$次の頂点(出次数 = 1)
$v_i$$1 \le v_i \le N$クエリ開始頂点
$K_i$$0 \le K_i \le 10^{18}$移動回数(最大値に注意)

入出力例

入力例1

6 3
2 3 4 2 6 5
1 1
1 3
1 7

出力例1

2
4
2

グラフ: $1\to2\to3\to4\to2$(サイクル $2\to3\to4\to2$、長さ3)、$5\to6\to5$(サイクル長2)。 クエリ $(1,7)$: テール1ステップで頂点2、残り $6\bmod3=0$ ステップ → 頂点2。

入力例2

5 4
2 3 1 5 4
1 0
1 3
4 1
4 10

出力例2

1
1
5
4

グラフ: $1\to2\to3\to1$(サイクル長3)、$4\to5\to4$(サイクル長2)。 クエリ $(4,10)$: $10\bmod2=0$ → 頂点4。

概念図: Functional Graph の ρ 形状と Binary Lifting

Functional Graph の ρ 形状(入力例1) サイクル頂点 テール頂点 別連結成分 2 3 4 cycle_len=3 1 dist=1 5 6 cycle_len=2 Binary Lifting テーブル: up[k][v] = v から 2^k 回移動した先 up[0][v] = f(v) (1 ステップ) up[1][v] = f(f(v)) (2 ステップ) = up[0][up[0][v]] up[k][v] = up[k-1][up[k-1][v]] (2^k ステップ) クエリ K=7 = 111₂: v = up[0][up[1][up[2][v]]] → 1+2+4=7 ステップ O(log K)

緑色の頂点 {2,3,4} がサイクル(長さ3)。頂点1はテール(dist_to_cycle=1)。 青色の {5,6} は別連結成分のサイクル(長さ2)。 下部パネルに Binary Lifting テーブルの漸化式とクエリ処理のビット展開を示す。

ヒント

ヒント1(方向性)

機能グラフの各連結成分は必ず ρ(ロー)形状になる:テール部分(サイクルへ向かう木の枝)とサイクル部分の組み合わせ。 $K \le 10^{18}$ のため直接シミュレーションは不可能(TLE)。

最もシンプルな解法は Binary Lifting(倍加法): $2^k$ ステップ先を前計算し、$K$ をビット展開して $O(\log K)$ でクエリ処理。 LOG = 62 で $K \le 10^{18}$ に対応できる($2^{62} > 4.6 \times 10^{18}$)。

ヒント2(アプローチ)

方法A: Binary Lifting のみ(推奨)

  1. up[k][v] = 頂点 $v$ から $2^k$ 回移動した先を前計算($O(N \log K)$)
  2. 漸化式: up[k][v] = up[k-1][up[k-1][v]]
  3. 各クエリ $(v,K)$ を $K$ のビット展開で処理($O(\log K)$)

方法B: サイクル検出 + mod 最適化(発展)

  1. 位相的ソートの逆用(in-degree BFS)でサイクル頂点を特定
  2. 各頂点 $v$ について dist_to_cycle と cycle_len を計算
  3. $K \ge \text{dist\_to\_cycle}[v]$ なら残り $(K-d)\bmod L$ ステップを mod 計算で $O(1)$
ヒント3(コードスケルトン)
# Binary Lifting テーブル構築
LOG = 62
up = [nxt[:]]        # up[0][v] = nxt[v](1回移動、1-indexed)
for k in range(1, LOG):
    prev = up[k - 1]
    cur = [0] * (N + 1)
    for v in range(1, N + 1):
        cur[v] = prev[prev[v]]  # 2^k = 2^(k-1) + 2^(k-1)
    up.append(cur)

def go(v, K):
    """頂点 v から K 回移動した先を O(log K) で返す"""
    for bit in range(LOG):
        if (K >> bit) & 1:   # K の bit 番目が立っている
            v = up[bit][v]   # 2^bit ステップ進む
    return v

# クエリ処理
for _ in range(Q):
    v, K = map(int, input().split())
    print(go(v, K))

模範解答

方法A: Binary Lifting のみ(シンプル・推奨)

import sys

def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N, Q = int(data[idx]), int(data[idx + 1])
    idx += 2

    # nxt[v] = 頂点 v の次の頂点(1-indexed)
    nxt = [0] * (N + 1)
    for v in range(1, N + 1):
        nxt[v] = int(data[idx])
        idx += 1

    # ─── Binary Lifting テーブル構築 O(N * LOG) ─────────────────
    # up[k][v] = 頂点 v から 2^k 回移動した先の頂点
    # K <= 10^18 < 2^62 なので LOG = 62 で全ビットをカバー
    LOG = 62
    up = [list(nxt)]   # up[0]: index 0 は番兵(使わない)
    for k in range(1, LOG):
        prev = up[k - 1]
        cur = [0] * (N + 1)
        for v in range(1, N + 1):
            cur[v] = prev[prev[v]]
        up.append(cur)

    # ─── クエリ処理 O(Q * LOG) ──────────────────────────────────
    # K をビット展開: K = sum(2^bit for bit where bit is set)
    # 各ビットに対応する 2^bit ステップを順番に適用
    out = []
    for _ in range(Q):
        v, K = int(data[idx]), int(data[idx + 1])
        idx += 2
        for bit in range(LOG):
            if (K >> bit) & 1:
                v = up[bit][v]
        out.append(v)

    sys.stdout.write('\n'.join(map(str, out)) + '\n')

solve()

方法B: サイクル検出 + mod 最適化(発展・完全版)

import sys
from collections import deque

def solve_cycle():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N, Q = int(data[idx]), int(data[idx + 1])
    idx += 2

    nxt = [0] * (N + 1)
    for v in range(1, N + 1):
        nxt[v] = int(data[idx])
        idx += 1

    # ─── サイクル頂点を in-degree BFS で特定 ────────────────────
    indeg = [0] * (N + 1)
    for v in range(1, N + 1):
        indeg[nxt[v]] += 1

    q = deque(v for v in range(1, N + 1) if indeg[v] == 0)
    on_cycle = [True] * (N + 1)
    on_cycle[0] = False
    while q:
        v = q.popleft()
        on_cycle[v] = False
        w = nxt[v]
        indeg[w] -= 1
        if indeg[w] == 0:
            q.append(w)

    # ─── 各サイクルの長さを計算 ─────────────────────────────────
    cycle_len = [0] * (N + 1)
    visited_c = [False] * (N + 1)
    for start in range(1, N + 1):
        if on_cycle[start] and not visited_c[start]:
            v, length = start, 0
            while True:
                visited_c[v] = True
                length += 1
                v = nxt[v]
                if v == start:
                    break
            v = start
            while True:
                cycle_len[v] = length
                v = nxt[v]
                if v == start:
                    break

    # ─── テール頂点の dist_to_cycle と cycle_entry をメモ化 ─────
    dist_to_cycle = [-1] * (N + 1)
    cycle_entry = [0] * (N + 1)

    def get_info(start):
        path = []
        v = start
        while not on_cycle[v] and dist_to_cycle[v] < 0:
            path.append(v)
            v = nxt[v]
        if on_cycle[v]:
            base_entry, base_dist = v, 0
        else:
            base_entry = cycle_entry[v]
            base_dist = dist_to_cycle[v]
        for i, u in enumerate(reversed(path)):
            dist_to_cycle[u] = base_dist + i + 1
            cycle_entry[u] = base_entry

    for v in range(1, N + 1):
        if not on_cycle[v]:
            if dist_to_cycle[v] < 0:
                get_info(v)
        else:
            dist_to_cycle[v] = 0
            cycle_entry[v] = v

    # ─── Binary Lifting(テール上のクエリ用) ────────────────────
    LOG = 62
    up = [list(nxt)]
    for k in range(1, LOG):
        prev = up[k - 1]
        cur = [0] * (N + 1)
        for v in range(1, N + 1):
            cur[v] = prev[prev[v]]
        up.append(cur)

    def go_lift(v, k):
        for bit in range(LOG):
            if (k >> bit) & 1:
                v = up[bit][v]
        return v

    out = []
    for _ in range(Q):
        v, K = int(data[idx]), int(data[idx + 1])
        idx += 2
        d = dist_to_cycle[v]
        if K < d:
            result = go_lift(v, K)
        else:
            entry = cycle_entry[v]
            rem = (K - d) % cycle_len[entry]
            result = go_lift(entry, rem)
        out.append(result)

    sys.stdout.write('\n'.join(map(str, out)) + '\n')

solve_cycle()

Step-by-Step 解説

Step 1: 機能グラフの ρ 形状を理解する

機能グラフは各頂点の出次数がちょうど 1 のグラフ。頂点数 $N$、辺数 $N$ なので、 各弱連結成分は必ずちょうど 1 つのサイクルを含む(鳩の巣原理)。

テール:  1 → 2 ─┐
                 ↓
サイクル: ┌── 2 ← 4
          └→ 3 ──┘    (cycle_len = 3)
  • サイクル頂点: グラフの「ループ」部分(dist_to_cycle = 0)
  • テール頂点: サイクルに繋がるが自身はサイクル外(dist_to_cycle > 0)

$K$ 回移動の動作: テール頂点 $v$ に対して、 $K < d_v$ ならテール内で完結、 $K \ge d_v$ ならサイクル入口から $(K - d_v) \bmod L$ ステップ。

Step 2: Binary Lifting テーブルの構築 $O(N \log K)$

倍加法の核心: $2^k$ ステップ先を前計算し、任意の $K$ を $O(\log K)$ で処理する。

$k$up[k][v] の意味漸化式
01 ステップ先 = $f(v)$up[0][v] = nxt[v]
12 ステップ先 = $f(f(v))$up[1][v] = up[0][up[0][v]]
24 ステップ先up[2][v] = up[1][up[1][v]]
k$2^k$ ステップ先up[k][v] = up[k-1][up[k-1][v]]

$K \le 10^{18} < 2^{60}$ だが余裕を持って LOG = 62 とする。 空間: $O(N \times 62) \approx 1.24 \times 10^7$ 整数。

Step 3: クエリを $O(\log K)$ で処理する

$K$ を 2 進数で展開し、立っているビットに対応する up[bit] を順次適用する。

def go(v, K):
    for bit in range(LOG):
        if (K >> bit) & 1:   # K の bit 番目が立っている
            v = up[bit][v]   # 2^bit ステップ進む
    return v

例: $K = 7 = 2^0 + 2^1 + 2^2$ → bit=0,1,2 で計 $1+2+4=7$ ステップ進む。

Step 4: サイクル頂点の特定(位相的ソート逆用)

in-degree 0 の頂点からキュー BFS でテール頂点を順次除去。 最終的に残った頂点集合 = サイクル頂点。

  1. 全頂点の in-degree を計算
  2. in-degree 0 の頂点をキューに積む(テール確定)
  3. 取り出した頂点の次の頂点の in-degree を -1。0 になればキューに追加
  4. キューが空になったとき、残りの頂点 = サイクル上

Step 5: 計算量分析

フェーズ時間計算量空間計算量
Binary Lifting 構築$O(N \cdot \mathrm{LOG})$$O(N \cdot \mathrm{LOG})$
サイクル検出(方法B)$O(N)$$O(N)$
クエリ処理$O(Q \cdot \mathrm{LOG})$$O(1)$ per query
合計$O((N+Q)\log K)$$O(N\log K)$

$N = Q = 2\times10^5$, LOG = 62 で合計 $\approx 2.5\times10^7$ 演算。PyPy で十分高速。

よくあるミス

ミス原因正しい書き方
LOG = 60 にする $K=10^{18}$、$2^{60}\approx1.15\times10^{18}$ で不足するケースがある LOG = 62($2^{62}>4\times10^{18}$)
0-indexed と 1-indexed の混在 f[0] を頂点 1 と混同しインデックスがずれる nxt[v](1-indexed)で統一し up[k][0]=0 を番兵に
up = [nxt] とすると参照共有 up[0] を後から変更すると nxt も変わる up = [list(nxt)] でコピーを作る
$K=0$ の特別処理を書いてしまう 全ビット不成立で $v$ がそのまま返る(正しい) 特別処理は不要
サイクル長 1(自己ループ)での mod (K - d) % 1 = 0 なのでサイクル入口がそのまま返る 正しく動作するが % 0 にならないよう注意
Python の再帰でスタックオーバーフロー get_info をナイーブな再帰で書くとテール長 $N$ で爆発 必ず反復(while ループ)でパス収集 → 逆順でメモ化

次のステップ

  • 発展問題1: K 回適用の逆関数(頂点 v の K ステップ前にいた可能性のある頂点を全列挙)。テール上の頂点の逆辿りは逆グラフの BFS/DFS で列挙可能だが、サイクル上は複数の候補がある。
  • 発展問題2: 辺に重みが付いた機能グラフで、K 回移動したときの重みの総和を求めよ。Binary Lifting テーブルを (next_vertex, weight_sum) のペアに拡張する。
  • 発展問題3: 機能グラフ上で「頂点 u と頂点 v が最初に合流する頂点と必要ステップ数」を求めよ(合流点問題・ρ 形状 LCA)。
  • AtCoder 関連問題: ABC 167 D「Teleporter」(機能グラフ + Binary Lifting の典型問題)、ABC 258 E「Packing Potions」。

自己評価