⚙️AlgorithmIntermediate

Functional Graph

Key Points

  • A functional graph is a directed graph where every node has exactly one outgoing edge, so repeatedly following edges from any start eventually loops into a cycle.
  • Each connected component consists of one directed cycle with directed trees feeding into the cycle, forming a ρ-shaped structure.
  • Floyd’s cycle-finding (tortoise and hare) detects the cycle and computes the entrance index μ and cycle length λ in O(μ + time and O(1) space.
  • To locate the exact cycle start after detection, reset one pointer to the start and advance both by one step until they meet.
  • Binary lifting precomputes 2^k-th successors to answer k-th successor queries in O(log k) time after O(n log K) preprocessing.
  • For permutations (bijective mappings), every node lies on some cycle and there are no trees feeding into cycles.
  • Functional graphs model iterated functions and state machines; they are common in problems about periodicity, “jump k steps,” and meeting points.
  • Typical complexities: O(n) to decompose all nodes into cycles and distances to cycles, and O(n log K) memory/time to support fast successor queries.

Prerequisites

  • Directed graphs and representationsYou need to understand nodes, directed edges, adjacency via next[v], and how to traverse graphs.
  • Function composition and iterationFunctional graphs encode f: V→V and repeated application f^{(k)}; this underpins orbits and successor queries.
  • Asymptotic analysis (Big-O)To reason about O(n), O(log k), and space-time trade-offs in preprocessing and queries.
  • Binary representation and bit operationsBinary lifting uses the binary expansion of k to select powers of two jumps.
  • Indexing conventions (0-based vs 1-based)Prevents off-by-one mistakes when mapping input to arrays and back.

Detailed Explanation

Tap terms for definitions

01Overview

A functional graph is a directed graph where each vertex has exactly one outgoing edge. You can think of it as the graph of a function f over a finite set of nodes: node v points to f(v). Because every node picks exactly one successor, edges act like a deterministic next-step rule. If you start at any node and follow the next pointers, you can never get stuck, and because there are only finitely many nodes, you must eventually revisit a node you’ve seen before; this creates a directed cycle. Every connected component of a functional graph therefore looks like one cycle, with directed trees (in-arborescences) that flow into that cycle. This ρ-shaped structure makes functional graphs surprisingly structured and efficient to work with.

These graphs show up frequently in algorithmic problems: in permutations (where every node also has exactly one incoming edge), iterated functions (like repeatedly applying f(x) = x^2 mod m), and finite-state processes that eventually become periodic. Core techniques include cycle detection (Floyd’s tortoise-and-hare), finding the cycle’s entrance and length, and binary lifting (jump pointers) to answer k-step successor queries quickly. With these tools, you can solve many problems about periodic states, cycle lengths, and fast iteration in near-linear time.

02Intuition & Analogies

Imagine a room for every person in a school, and each person tapes an arrow on their door pointing to exactly one other person’s door: “the person I text next.” If you start at any door and keep following arrows, you’ll never run out of arrows—they always point somewhere—so after enough steps, you must eventually return to a door you’ve already visited, forming a loop of doors that point to each other. That loop is the cycle. Everyone else who doesn’t start inside the loop eventually walks into it through a chain of arrows—those chains are like hallways feeding into the roundabout. Each connected piece of the school map therefore looks like hallways (trees) draining into a roundabout (cycle).

A second analogy: rivers. Every drop of rain flows to exactly one downstream location. If we were on a finite spherical world made of channels, following the flow from any drop eventually falls into a whirlpool (cycle). Small streams feed into bigger streams, which feed into the whirlpool; the whirlpool keeps circulating forever. If you count how many steps it takes a leaf to reach the whirlpool, that’s the distance to the cycle; how many steps to go around the whirlpool once is the cycle length.

Finally, think of pressing a button on a simple device: it always goes to one next screen. Repeated presses define a sequence of screens. Eventually, the device starts repeating a sequence of screens—this is the periodic part—after some initial wandering screens—the preperiod. Functional graphs formalize this exact behavior, letting us compute how long until repetition and where the cycle begins.

03Formal Definition

A functional graph is a directed graph G = (V, E) on a finite set V where every vertex v ∈ V has out-degree 1. Equivalently, there exists a function f: such that E = {(v, f(v)) | v ∈ V}. If we define an orbit by ∈ V and ), then the sequence () is eventually periodic: there exist integers μ ≥ 0 and λ ≥ 1 such that for all t ≥ The smallest such μ is the preperiod length (entrance time), and the smallest such λ is the period (cycle length). The graph decomposes into disjoint weakly connected components; each component contains exactly one directed cycle, and every other vertex in the component lies on a directed path leading into that cycle. Define the k-fold composition by (v) = v and (v) = f((v)). Many queries are of the form: given v and k, compute (v). Binary lifting precomputes jump pointers up[j][v] = (v), enabling (v) evaluation using the binary expansion of k. For cycle detection from a starting node, Floyd’s algorithm runs two pointers at different speeds and exploits eventual periodicity to detect and characterize cycles in O(μ + time and O(1) space.

04When to Use

Use functional graph techniques whenever you have a deterministic “next state” function over a finite set and you need to understand long-term behavior or fast-forward many steps. Typical cases include: permutations (the function is bijective, so every node lies on some cycle), iterated modular functions (e.g., f(x) = ax + b mod m, or f(x) = x^2 mod m), cellular automata with finite state per cell aggregated as a single state, and problems about repeated transitions in automata or state machines.

Operationally, apply Floyd’s cycle detection if you need to find the cycle that a specific starting node enters and want O(1) memory. Use full graph decomposition (marking cycle IDs, distances to cycles, and positions within cycles) when you must answer many queries about many nodes, such as “how many steps from v to any cycle?” or “are u and v in the same cycle?” Use binary lifting when you need to answer many k-th successor queries efficiently, especially for large k (up to 10^{18}). If the mapping changes over time (dynamic updates), precomputed jump tables may become invalid; in that case, consider different data structures or recomputation strategies.

⚠️Common Mistakes

  • Running Floyd’s algorithm from every node unnecessarily. Floyd on one start only reveals the cycle in that start’s component; to cover all nodes, decompose the whole graph in O(n) instead of O(n^2).
  • Off-by-one and indexing errors (0-based vs 1-based). Decide a convention and apply it consistently; convert inputs immediately.
  • Using an insufficient LOG in binary lifting. If k can be up to 10^{18}, you need LOG ≥ 60; otherwise, high bits of k are ignored and answers are wrong.
  • Storing k in 32-bit integers. Always use 64-bit (long long) for large k.
  • Forgetting self-loops and 2-cycles as valid cycles. Ensure cycle extraction handles cycles of length 1 and 2.
  • Recursive DFS that overflows the stack on large n. Prefer iterative exploration with an explicit stack for n up to 2e5 or higher.
  • Memory blowups from vector<vector<int>> with large n and LOG. Use a flat array up[LOG][n] or vector<array<int,LOG>> to reduce overhead.
  • Assuming permutation properties (every node also has in-degree 1) when the graph is not a permutation. In general functional graphs, in-degrees vary; trees feed into cycles. Treat tails and cycles separately.

Key Formulas

Out-degree property

Explanation: Every node in a functional graph has exactly one outgoing edge. This encodes the idea that the next state is uniquely determined.

Edge count

Explanation: Because each of the |V| nodes contributes exactly one outgoing edge, the total number of edges equals the number of vertices.

Orbit recurrence

Explanation: Defines the sequence of visited nodes when repeatedly applying the function f starting from . This is the core process observed in functional graphs.

Eventual periodicity

Explanation: Any orbit eventually repeats with period λ after an initial tail of length This is guaranteed because the state space is finite.

Cycle length

Explanation: The period λ is the smallest positive number of steps needed to return to the cycle entrance. It measures the cycle size.

Entrance index

Explanation: The preperiod μ is the earliest time after which the sequence becomes periodic. It counts steps before entering the cycle.

k-fold composition

Explanation: Defines repeated application of f. The value (v) is the k-th successor of v.

Binary lifting recurrence

Explanation: If u(v) is the 2^k-th successor, then composing it with itself yields the 2^{k+1}-th successor. This enables doubling steps.

Binary expansion for jumps

Explanation: Decompose k into powers of two and apply the corresponding precomputed jumps where . This yields O(log k) query time.

Floyd meeting condition

Explanation: In Floyd’s algorithm, when the tortoise (i steps) equals the hare (2i steps), the difference lies in the cycle. This implies periodicity with period λ and leads to finding μ and

Permutation cycle partition

Explanation: If f is a permutation on n elements, the graph is a disjoint union of cycles whose lengths sum to n. There are no tails in this case.

Complexity Analysis

Let n = and consider a single start node . Floyd’s cycle detection runs in O(μ + time because the tortoise and hare pointers first converge within at most μ steps to reach the cycle, and then within λ steps they meet inside the cycle. The algorithm uses O(1) additional memory: it stores only a few indices and performs random access next[v] lookups. To label every node with its cycle information and distance to the cycle, you can traverse each node once using an iterative stack and visitation states (unvisited/visiting/done). Each edge is examined at most a constant number of times, giving O(n) time and O(n) space for arrays such as state, distance-to-cycle, cycle-id, and optional position-in-cycle. Binary lifting supports fast k-th successor queries. Preprocessing builds up[j][v] = (v) for in O(n log K) time and O(n log K) space, where K is an upper bound on query k (often , so LO). Each query decomposes k into powers of two and applies at most LOG jumps, costing O(log K) time with O(1) extra space per query. If K is bounded by n (e.g., you only need up to n steps), setting LOG = ⌈log2 n⌉ can reduce memory. Trade-offs: Floyd’s method is ideal when you only need the cycle for one start and must keep memory minimal. Full decomposition is best for many queries over all nodes. Binary lifting is valuable for large k, but memory can be substantial (e.g., and LO implies ~12 million integers). Optimize memory by using 32-bit ints for node indices (when ), a flat up[LOG][n] layout, or compressing LOG to the maximum necessary bit-length of k in your problem.

Code Examples

Floyd’s cycle detection from a single start (μ and λ)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Floyd's cycle-finding algorithm for a functional graph
5// next[v] is the successor of v, 0 <= v < n
6// Given start, computes (mu, lambda), where mu is the index of first cycle node
7// along the orbit starting from start, and lambda is the cycle length.
8struct CycleInfo {
9 int mu; // steps before entering the cycle
10 int lambda; // cycle length
11 int cycleStartNode; // one node on the cycle (the entrance node)
12};
13
14CycleInfo floyd_cycle(const vector<int>& next, int start) {
15 // 1) Phase 1: find meeting point inside the cycle (if any)
16 int tortoise = next[start]; // f(x0)
17 int hare = next[next[start]]; // f(f(x0))
18 while (tortoise != hare) {
19 tortoise = next[tortoise];
20 hare = next[next[hare]];
21 }
22
23 // 2) Phase 2: find mu (cycle entrance index)
24 int mu = 0;
25 tortoise = start; // reset tortoise to start
26 while (tortoise != hare) {
27 tortoise = next[tortoise];
28 hare = next[hare];
29 ++mu;
30 }
31
32 // 3) Phase 3: find lambda (cycle length)
33 int lambda = 1;
34 hare = next[tortoise];
35 while (tortoise != hare) {
36 hare = next[hare];
37 ++lambda;
38 }
39
40 return {mu, lambda, tortoise}; // tortoise is at the cycle entrance node
41}
42
43int main() {
44 // Example functional graph on nodes [0..7]
45 // 0->1->2->3->1 (cycle 1-2-3), 4->5->6->4 (cycle 4-5-6), 7->6
46 vector<int> next = {1,2,3,1,5,6,4,6};
47
48 int start = 0;
49 CycleInfo info = floyd_cycle(next, start);
50
51 cout << "Start: " << start << "\n";
52 cout << "mu (steps to enter cycle): " << info.mu << "\n";
53 cout << "lambda (cycle length): " << info.lambda << "\n";
54 cout << "one cycle entrance node: " << info.cycleStartNode << "\n";
55
56 // Demonstrate orbit until first repeat
57 int v = start;
58 cout << "Orbit: ";
59 for (int i = 0; i < info.mu + info.lambda + 2; ++i) {
60 cout << v << (i+1 < info.mu + info.lambda + 2 ? ' ' : '\n');
61 v = next[v];
62 }
63 return 0;
64}
65

This program implements Floyd’s tortoise-and-hare algorithm. Phase 1 finds a meeting point inside the cycle by moving one pointer at speed 1 and another at speed 2. Phase 2 resets one pointer to the start and advances both at speed 1; their meeting point is the cycle entrance and the number of steps taken is μ. Phase 3 loops the fast pointer around the cycle to count λ. The example graph has a 3-cycle (1,2,3). Starting at 0, μ=1 and λ=3.

Time: O(μ + λ)Space: O(1)
Decompose the entire functional graph: cycle IDs, positions, and distance to cycle
1#include <bits/stdc++.h>
2using namespace std;
3
4// Decompose a functional graph into cycles and tails.
5// For each node v, compute:
6// - comp_id[v]: which cycle component it belongs to (0..C-1)
7// - dist_to_cycle[v]: steps to reach the cycle (0 if v is on a cycle)
8// - pos_in_cycle[v]: position in the cycle (0..len-1) if on cycle, else -1
9// This is done iteratively to avoid recursion stack issues.
10
11struct Decomp {
12 vector<int> comp_id; // component (cycle) id per node
13 vector<int> dist_to_cycle; // distance to cycle per node
14 vector<int> pos_in_cycle; // position in cycle or -1
15 vector<int> cycle_len; // length for each component id
16};
17
18Decomp decompose_functional_graph(const vector<int>& next) {
19 int n = (int)next.size();
20 vector<int> state(n, 0); // 0=unvisited, 1=visiting, 2=done
21 vector<int> comp_id(n, -1), dist(n, 0), pos(n, -1);
22 vector<int> cycle_len; // indexed by component id
23
24 vector<int> stk; stk.reserve(n);
25
26 int cid = 0;
27 for (int s = 0; s < n; ++s) if (state[s] == 0) {
28 int v = s;
29 // Walk until we hit a visited node
30 while (state[v] == 0) {
31 state[v] = 1; // visiting
32 stk.push_back(v);
33 v = next[v];
34 }
35
36 if (state[v] == 1) {
37 // Found a new cycle: extract nodes from stack until we reach v
38 vector<int> cyc;
39 while (true) {
40 int u = stk.back(); stk.pop_back();
41 cyc.push_back(u);
42 if (u == v) break;
43 }
44 reverse(cyc.begin(), cyc.end());
45
46 // Assign cycle nodes
47 int L = (int)cyc.size();
48 for (int i = 0; i < L; ++i) {
49 int u = cyc[i];
50 comp_id[u] = cid;
51 pos[u] = i; // position in cycle
52 dist[u] = 0; // on cycle
53 state[u] = 2; // done
54 }
55 cycle_len.push_back(L);
56 ++cid;
57 }
58
59 // Propagate information back along the remaining stack (tail nodes)
60 while (!stk.empty()) {
61 int u = stk.back(); stk.pop_back();
62 int w = next[u];
63 comp_id[u] = comp_id[w];
64 pos[u] = -1; // not on the cycle
65 dist[u] = dist[w] + 1;
66 state[u] = 2;
67 }
68 }
69
70 return {comp_id, dist, pos, cycle_len};
71}
72
73int main() {
74 // Example graph: 0->1->2->3->1, 4->5->6->4, 7->6
75 vector<int> next = {1,2,3,1,5,6,4,6};
76
77 Decomp D = decompose_functional_graph(next);
78
79 int n = (int)next.size();
80 cout << "node comp dist pos\n";
81 for (int v = 0; v < n; ++v) {
82 cout << v << ' ' << D.comp_id[v] << ' ' << D.dist_to_cycle[v] << ' ' << D.pos_in_cycle[v] << '\n';
83 }
84
85 cout << "\nCycle lengths per component id:\n";
86 for (int c = 0; c < (int)D.cycle_len.size(); ++c) {
87 cout << "comp " << c << ": length = " << D.cycle_len[c] << '\n';
88 }
89}
90

This code decomposes the entire graph iteratively. It detects cycles by encountering a node in the visiting state and extracts the cycle nodes from the exploration stack. These nodes get position-in-cycle and distance 0. The remaining nodes in the stack form tails; their component ID is inherited from their successor and their distance-to-cycle is one plus the successor’s distance. The result provides per-node component, distance to cycle, and cycle positions.

Time: O(n)Space: O(n)
Binary lifting (jump pointers) for k-th successor queries
1#include <bits/stdc++.h>
2using namespace std;
3
4// Precompute jump pointers up[j][v] = 2^j-th successor of v
5// and answer queries of the form: given (v, k), compute f^{(k)}(v).
6
7struct JumpTable {
8 int n;
9 int LOG; // number of levels (e.g., 60 for k up to 1e18)
10 vector<vector<int>> up; // up[j][v]
11};
12
13JumpTable build_jump_table(const vector<int>& next, int LOG) {
14 int n = (int)next.size();
15 vector<vector<int>> up(LOG, vector<int>(n));
16 // Base: 2^0-th successor is the immediate successor
17 for (int v = 0; v < n; ++v) up[0][v] = next[v];
18 // Doubling
19 for (int j = 1; j < LOG; ++j) {
20 for (int v = 0; v < n; ++v) {
21 up[j][v] = up[j-1][ up[j-1][v] ];
22 }
23 }
24 return {n, LOG, move(up)};
25}
26
27int kth_successor(const JumpTable& JT, int v, unsigned long long k) {
28 for (int j = 0; j < JT.LOG; ++j) {
29 if (k & (1ULL << j)) v = JT.up[j][v];
30 }
31 return v;
32}
33
34int main() {
35 ios::sync_with_stdio(false);
36 cin.tie(nullptr);
37
38 // Example graph: 0->1->2->3->1, 4->5->6->4, 7->6
39 vector<int> next = {1,2,3,1,5,6,4,6};
40
41 // Suppose k can be up to 1e18, choose LOG=60
42 const int LOG = 60;
43 JumpTable JT = build_jump_table(next, LOG);
44
45 // Example queries
46 vector<pair<int, unsigned long long>> queries = {
47 {0, 0}, {0, 1}, {0, 2}, {0, 5}, // move from node 0 by k steps
48 {7, 1}, {7, 2}, {7, 10} // move from node 7 by k steps
49 };
50
51 for (auto [v, k] : queries) {
52 int u = kth_successor(JT, v, k);
53 cout << "k-th successor: from " << v << " by k=" << k << " => " << u << '\n';
54 }
55
56 return 0;
57}
58

The jump table precomputes the 2^j-th successor for each node. To jump k steps, decompose k into binary and apply the corresponding up[j] where the bit is 1. This answers each query in O(log k) time. Using LOG=60 supports k up to about 10^{18}.

Time: Preprocessing O(n log K), per query O(log K)Space: O(n log K)