Functional graphs
A functional graph gives every vertex exactly one outgoing edge, so each component is a single cycle with trees hanging into it. This structure allows cycle detection in O(n), jump pointers for huge steps and fast distance queries.
Learning goals
- Describe the shape of a functional graph
- Find the cycle vertices in linear time
- Decompose the rest into in-trees with distances to the cycle
- Answer k-th successor and distance queries with binary lifting and cycle arithmetic
Prerequisites
Pointer chasing, pseudo-random number generators, “where do I end after a million steps?” and permutation cycles all describe a function applied again and again. The graph of such a function is so regular that a million steps cost almost nothing.
Cycle detection
A **functional graph** has exactly one outgoing edge per vertex: next[v]. It has n vertices and n edges. Following next from any vertex must eventually repeat a vertex, so every component contains **exactly one cycle**, possibly a self-loop, and everything else is a tree whose edges point toward the cycle. The walk from a vertex looks like the Greek letter ρ: a tail, then a loop.
To find all cycle vertices in O(n) use a three-state walk. From each unvisited vertex, follow next, marking vertices as **on the current walk**. If the walk reaches a vertex already on the current walk, the walk has closed on itself and everything from that vertex around is a cycle. If it reaches a finished vertex, the walk was only a tail. Afterwards mark all vertices of the walk as finished.
Open the animation on the default array next = [1, 2, 0, 2, 3, 4, 6, 6]. Vertices 0, 1, 2 form a cycle of length 3; vertex 6 is a self-loop, a cycle of length 1; vertices 3, 4, 5 and 7 are tails. Floyd's tortoise and hare finds one cycle with O(1) extra memory, but the 3-state walk is the right tool when you need every cycle.
1static boolean[] onCycle(int[] next) { // next[v] = the single successor of v2int n = next.length;3int[] state = new int[n]; // 0 new, 1 on the current walk, 2 finished4boolean[] cyc = new boolean[n];5for (int s = 0; s < n; s++) {6int v = s;7while (state[v] == 0) { state[v] = 1; v = next[v]; }8if (state[v] == 1) { // the walk closed on itself: v lies on a new cycle9for (int u = v; ; u = next[u]) { cyc[u] = true; if (next[u] == v) break; }10}11for (int u = s; state[u] == 1; u = next[u]) state[u] = 2;12}13return cyc;14}
Worst
O(n)
Space
O(n)
Functional graphs: one successor per vertex, cycles with trees, jump pointers
Interactive · edit the inputdirected graph · 8 vertices · 8 edges
steps to reach a cycle
Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it.
1static int[] stepsToCycle(int[] next) { // 0 for vertices on a cycle2int n = next.length;3int[] state = new int[n], dist = new int[n]; // state: 0 new, 1 on the current walk, 2 finished4for (int s = 0; s < n; s++) {5if (state[s] != 0) continue;6List<Integer> walk = new ArrayList<>();7int v = s;8while (state[v] == 0) { state[v] = 1; walk.add(v); v = next[v]; }9int from = walk.size(); // answers are filled backwards from here10if (state[v] == 1) { // the walk met itself: a new cycle11int at = walk.indexOf(v);12for (int i = at; i < walk.size(); i++) dist[walk.get(i)] = 0;13from = at;14}15for (int i = from - 1; i >= 0; i--) dist[walk.get(i)] = dist[next[walk.get(i)]] + 1;16for (int u : walk) state[u] = 2;17}18return dist;19}20static int[][] jumpTable(int[] next, int levels) { // up[j][v] = the vertex reached after 2^j steps21int n = next.length;22int[][] up = new int[levels][];23up[0] = next.clone();24for (int j = 1; j < levels; j++) {25up[j] = new int[n];26for (int v = 0; v < n; v++) up[j][v] = up[j - 1][up[j - 1][v]];27}28return up;29}30static int kthSuccessor(int[][] up, int v, long k) {31for (int j = 0; j < up.length; j++) if ((k >> j & 1) == 1) v = up[j][v];32return v;33}
Live state
n
8
Execution trace (dry run table)Show 22 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=8 | Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it. |
| 2 | L8 | start=0 walk=[0] | Walk from 0: mark 0, then follow its arrow to 1. |
| 3 | L8 | start=0 walk=[0, 1] | Walk from 0: mark 1, then follow its arrow to 2. |
| 4 | L8 | start=0 walk=[0, 1, 2] | Walk from 0: mark 2, then follow its arrow to 0. |
| 5 | L11 | cycle=[0, 1, 2] | The walk reached 0, which is on the current walk: the vertices [0, 1, 2] form a cycle of length 3. Their distance to the cycle is 0. |
| 6 | L15 | distances=[0, 0, 0] | No tail: every vertex of this walk is on the cycle. |
| 7 | L8 | start=3 walk=[3] | Walk from 3: mark 3, then follow its arrow to 2. |
| 8 | L11 | met=2 | The walk reached 2, which was finished earlier: the new vertices lead into it. |
| 9 | L15 | distances=[0, 0, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 10 | L8 | start=4 walk=[4] | Walk from 4: mark 4, then follow its arrow to 3. |
| 11 | L11 | met=3 | The walk reached 3, which was finished earlier: the new vertices lead into it. |
| 12 | L15 | distances=[0, 0, 0, 1, 2] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 13 | L8 | start=5 walk=[5] | Walk from 5: mark 5, then follow its arrow to 4. |
| 14 | L11 | met=4 | The walk reached 4, which was finished earlier: the new vertices lead into it. |
| 15 | L15 | distances=[0, 0, 0, 1, 2, 3] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 16 | L8 | start=6 walk=[6] | Walk from 6: mark 6, then follow its arrow to 6. |
| 17 | L11 | cycle=[6] | The walk reached 6, which is on the current walk: the vertices [6] form a cycle of length 1. Their distance to the cycle is 0. |
| 18 | L15 | distances=[0, 0, 0, 1, 2, 3, 0] | No tail: every vertex of this walk is on the cycle. |
| 19 | L8 | start=7 walk=[7] | Walk from 7: mark 7, then follow its arrow to 6. |
| 20 | L11 | met=6 | The walk reached 6, which was finished earlier: the new vertices lead into it. |
| 21 | L15 | distances=[0, 0, 0, 1, 2, 3, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 22 | L18 | return=[0, 0, 0, 1, 2, 3, 0, 1] cycles=2 | 2 cycles found. Distances to the cycle: [0, 0, 0, 1, 2, 3, 0, 1] (0 = on a cycle). Each vertex is walked once: O(n). |
In-tree decomposition
Remove the cycle edges and what remains of each component is a set of **in-trees**: trees whose root is a cycle vertex and whose edges point toward the root. For every vertex we want two things: its **distance to the cycle** (the tail length) and its **entry**, the cycle vertex where it first lands.
Both are filled in one linear pass. Cycle vertices have distance 0 and are their own entries. For another vertex, walk next until a vertex with a known answer is reached, keeping the walk on a stack; then pop the stack and set dist[u] = dist[next[u]] + 1 and entry[u] = entry[next[u]]. Every vertex is pushed at most once, so the whole table costs O(n).
With the default array: vertex 3 goes to 2 (distance 1, entry 2), vertex 4 to 3 then 2 (distance 2, entry 2) and 5 has distance 3, all entering the cycle at 2. Vertex 7 goes to the self-loop 6 (distance 1, entry 6). These two arrays are the foundation of the distance queries below, and an in-tree is a perfect candidate for tree algorithms such as LCA (Chapter 42).
1// for each vertex: how many steps until it first lands on a cycle, and the cycle vertex where it lands2static int[][] tails(int[] next, boolean[] onCycle) {3int n = next.length;4int[] dist = new int[n], entry = new int[n];5boolean[] done = new boolean[n];6for (int v = 0; v < n; v++) if (onCycle[v]) { done[v] = true; entry[v] = v; }7for (int s = 0; s < n; s++) {8Deque<Integer> path = new ArrayDeque<>();9int v = s;10while (!done[v]) { path.push(v); v = next[v]; } // walk until a vertex whose answer is known11while (!path.isEmpty()) { // then fill the answers backwards12int u = path.pop();13dist[u] = dist[next[u]] + 1;14entry[u] = entry[next[u]];15done[u] = true;16}17}18return new int[][]{dist, entry};19}
Worst
O(n)
Space
O(n)
Binary lifting
How far does a vertex get after k steps when k is huge? Simulating costs O(k). **Binary lifting** precomputes up[j][v], the vertex reached after 2^j steps from v. Level 0 is next itself, and up[j][v] = up[j − 1][ up[j − 1][v] ]: two steps of length 2^(j−1). A table with ⌈log2 K⌉ levels handles every k below 2^levels.
Building the table is O(n log K) time and memory. The idea works because composing a function with itself is associative, which is why the same trick appears for ancestors in trees (Chapter 42) and for any “repeat an operation k times” query where one step can be combined with another.
Run the animation with the jump option. The preset “Jump 100 steps from vertex 5” walks 5 → 4 → 3 → 2 to reach the cycle after 3 steps, then goes round the 3-cycle for the remaining 97 steps, which is 32 full laps and 1 more step, ending at vertex 0.
1static int[][] jumpTable(int[] next, long maxSteps) { // up[j][v] = the vertex after 2^j steps from v2int n = next.length, levels = 1;3while ((1L << levels) <= maxSteps) levels++;4int[][] up = new int[levels][];5up[0] = next.clone();6for (int j = 1; j < levels; j++) {7up[j] = new int[n];8for (int v = 0; v < n; v++) up[j][v] = up[j - 1][up[j - 1][v]];9}10return up;11}
Worst
O(n log K) to build
Space
O(n log K)
Functional graphs: one successor per vertex, cycles with trees, jump pointers
Interactive · edit the inputdirected graph · 8 vertices · 8 edges
steps to reach a cycle
Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it.
1static int[] stepsToCycle(int[] next) { // 0 for vertices on a cycle2int n = next.length;3int[] state = new int[n], dist = new int[n]; // state: 0 new, 1 on the current walk, 2 finished4for (int s = 0; s < n; s++) {5if (state[s] != 0) continue;6List<Integer> walk = new ArrayList<>();7int v = s;8while (state[v] == 0) { state[v] = 1; walk.add(v); v = next[v]; }9int from = walk.size(); // answers are filled backwards from here10if (state[v] == 1) { // the walk met itself: a new cycle11int at = walk.indexOf(v);12for (int i = at; i < walk.size(); i++) dist[walk.get(i)] = 0;13from = at;14}15for (int i = from - 1; i >= 0; i--) dist[walk.get(i)] = dist[next[walk.get(i)]] + 1;16for (int u : walk) state[u] = 2;17}18return dist;19}20static int[][] jumpTable(int[] next, int levels) { // up[j][v] = the vertex reached after 2^j steps21int n = next.length;22int[][] up = new int[levels][];23up[0] = next.clone();24for (int j = 1; j < levels; j++) {25up[j] = new int[n];26for (int v = 0; v < n; v++) up[j][v] = up[j - 1][up[j - 1][v]];27}28return up;29}30static int kthSuccessor(int[][] up, int v, long k) {31for (int j = 0; j < up.length; j++) if ((k >> j & 1) == 1) v = up[j][v];32return v;33}
Live state
n
8
Execution trace (dry run table)Show 22 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=8 | Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it. |
| 2 | L8 | start=0 walk=[0] | Walk from 0: mark 0, then follow its arrow to 1. |
| 3 | L8 | start=0 walk=[0, 1] | Walk from 0: mark 1, then follow its arrow to 2. |
| 4 | L8 | start=0 walk=[0, 1, 2] | Walk from 0: mark 2, then follow its arrow to 0. |
| 5 | L11 | cycle=[0, 1, 2] | The walk reached 0, which is on the current walk: the vertices [0, 1, 2] form a cycle of length 3. Their distance to the cycle is 0. |
| 6 | L15 | distances=[0, 0, 0] | No tail: every vertex of this walk is on the cycle. |
| 7 | L8 | start=3 walk=[3] | Walk from 3: mark 3, then follow its arrow to 2. |
| 8 | L11 | met=2 | The walk reached 2, which was finished earlier: the new vertices lead into it. |
| 9 | L15 | distances=[0, 0, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 10 | L8 | start=4 walk=[4] | Walk from 4: mark 4, then follow its arrow to 3. |
| 11 | L11 | met=3 | The walk reached 3, which was finished earlier: the new vertices lead into it. |
| 12 | L15 | distances=[0, 0, 0, 1, 2] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 13 | L8 | start=5 walk=[5] | Walk from 5: mark 5, then follow its arrow to 4. |
| 14 | L11 | met=4 | The walk reached 4, which was finished earlier: the new vertices lead into it. |
| 15 | L15 | distances=[0, 0, 0, 1, 2, 3] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 16 | L8 | start=6 walk=[6] | Walk from 6: mark 6, then follow its arrow to 6. |
| 17 | L11 | cycle=[6] | The walk reached 6, which is on the current walk: the vertices [6] form a cycle of length 1. Their distance to the cycle is 0. |
| 18 | L15 | distances=[0, 0, 0, 1, 2, 3, 0] | No tail: every vertex of this walk is on the cycle. |
| 19 | L8 | start=7 walk=[7] | Walk from 7: mark 7, then follow its arrow to 6. |
| 20 | L11 | met=6 | The walk reached 6, which was finished earlier: the new vertices lead into it. |
| 21 | L15 | distances=[0, 0, 0, 1, 2, 3, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 22 | L18 | return=[0, 0, 0, 1, 2, 3, 0, 1] cycles=2 | 2 cycles found. Distances to the cycle: [0, 0, 0, 1, 2, 3, 0, 1] (0 = on a cycle). Each vertex is walked once: O(n). |
Jump queries
With the jump table in hand, the vertex after k steps is found by reading the bits of k: for every set bit j, replace v by up[j][v]. A query costs O(log k), so the answer for k = 10^9 takes 30 table lookups.
There is an alternative that needs no table and uses the structure instead: simulate at most n steps (after which the walker is certainly on its cycle), then reduce the remaining steps modulo the cycle length. This costs O(n) per query but only O(n) memory, and it is the better choice when there are few queries. Binary lifting wins when there are many queries or when the function is not a plain successor (for example, when each step also carries a weight to be combined).
The Java method below requires k < 2^levels of the table built in the previous section. Check it against the simulation on small inputs before trusting it for large ones; the verification of this lesson did exactly that, with k up to one billion.
1static int kthSuccessor(int[][] up, int v, long k) { // needs k < 2^up.length2for (int j = 0; j < up.length; j++) if ((k >> j & 1) == 1) v = up[j][v];3return v;4}
Worst
O(log k) per query
Space
O(1) extra
Functional graphs: one successor per vertex, cycles with trees, jump pointers
Interactive · edit the inputdirected graph · 8 vertices · 8 edges
steps to reach a cycle
Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it.
1static int[] stepsToCycle(int[] next) { // 0 for vertices on a cycle2int n = next.length;3int[] state = new int[n], dist = new int[n]; // state: 0 new, 1 on the current walk, 2 finished4for (int s = 0; s < n; s++) {5if (state[s] != 0) continue;6List<Integer> walk = new ArrayList<>();7int v = s;8while (state[v] == 0) { state[v] = 1; walk.add(v); v = next[v]; }9int from = walk.size(); // answers are filled backwards from here10if (state[v] == 1) { // the walk met itself: a new cycle11int at = walk.indexOf(v);12for (int i = at; i < walk.size(); i++) dist[walk.get(i)] = 0;13from = at;14}15for (int i = from - 1; i >= 0; i--) dist[walk.get(i)] = dist[next[walk.get(i)]] + 1;16for (int u : walk) state[u] = 2;17}18return dist;19}20static int[][] jumpTable(int[] next, int levels) { // up[j][v] = the vertex reached after 2^j steps21int n = next.length;22int[][] up = new int[levels][];23up[0] = next.clone();24for (int j = 1; j < levels; j++) {25up[j] = new int[n];26for (int v = 0; v < n; v++) up[j][v] = up[j - 1][up[j - 1][v]];27}28return up;29}30static int kthSuccessor(int[][] up, int v, long k) {31for (int j = 0; j < up.length; j++) if ((k >> j & 1) == 1) v = up[j][v];32return v;33}
Live state
n
8
Execution trace (dry run table)Show 22 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | n=8 | Every vertex has exactly one outgoing arrow, so walking from any vertex either reaches a vertex seen before. Each connected piece is one cycle with trees hanging into it. |
| 2 | L8 | start=0 walk=[0] | Walk from 0: mark 0, then follow its arrow to 1. |
| 3 | L8 | start=0 walk=[0, 1] | Walk from 0: mark 1, then follow its arrow to 2. |
| 4 | L8 | start=0 walk=[0, 1, 2] | Walk from 0: mark 2, then follow its arrow to 0. |
| 5 | L11 | cycle=[0, 1, 2] | The walk reached 0, which is on the current walk: the vertices [0, 1, 2] form a cycle of length 3. Their distance to the cycle is 0. |
| 6 | L15 | distances=[0, 0, 0] | No tail: every vertex of this walk is on the cycle. |
| 7 | L8 | start=3 walk=[3] | Walk from 3: mark 3, then follow its arrow to 2. |
| 8 | L11 | met=2 | The walk reached 2, which was finished earlier: the new vertices lead into it. |
| 9 | L15 | distances=[0, 0, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 10 | L8 | start=4 walk=[4] | Walk from 4: mark 4, then follow its arrow to 3. |
| 11 | L11 | met=3 | The walk reached 3, which was finished earlier: the new vertices lead into it. |
| 12 | L15 | distances=[0, 0, 0, 1, 2] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 13 | L8 | start=5 walk=[5] | Walk from 5: mark 5, then follow its arrow to 4. |
| 14 | L11 | met=4 | The walk reached 4, which was finished earlier: the new vertices lead into it. |
| 15 | L15 | distances=[0, 0, 0, 1, 2, 3] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 16 | L8 | start=6 walk=[6] | Walk from 6: mark 6, then follow its arrow to 6. |
| 17 | L11 | cycle=[6] | The walk reached 6, which is on the current walk: the vertices [6] form a cycle of length 1. Their distance to the cycle is 0. |
| 18 | L15 | distances=[0, 0, 0, 1, 2, 3, 0] | No tail: every vertex of this walk is on the cycle. |
| 19 | L8 | start=7 walk=[7] | Walk from 7: mark 7, then follow its arrow to 6. |
| 20 | L11 | met=6 | The walk reached 6, which was finished earlier: the new vertices lead into it. |
| 21 | L15 | distances=[0, 0, 0, 1, 2, 3, 0, 1] | Fill the tail backwards: each tail vertex is one step further from the cycle than its successor. |
| 22 | L18 | return=[0, 0, 0, 1, 2, 3, 0, 1] cycles=2 | 2 cycles found. Distances to the cycle: [0, 0, 0, 1, 2, 3, 0, 1] (0 = on a cycle). Each vertex is walked once: O(n). |
Distance queries
**Distance query:** the smallest t ≥ 0 with next^t(u) = v, or “unreachable”. The answer depends on where v sits. If v is on a cycle, u first walks its tail (dist[u] steps) to its entry vertex, then travels around the cycle from the entry to v: (pos[v] − pos[entry[u]]) mod cycleLength more steps, provided both are on the same cycle. If v is on a tail, u must pass through it: it needs dist[u] ≥ dist[v], and the vertex reached after dist[u] − dist[v] steps must be v.
The class below precomputes the cycle positions, lengths and ids, and the tails from the previous sections in O(n). A query on a cycle target is O(1); a query on a tail target walks dist[u] − dist[v] steps (with binary lifting it becomes O(log n)).
The structure answers many questions: “do these two vertices collide?”, “after how many steps does the sequence repeat?” (tail length plus cycle length) and “what is the period?”. The verification compared every pair of vertices of thousands of random functions with direct simulation.
| Target v is… | Steps from u to v |
|---|---|
| on a cycle, same cycle as u's entry | dist[u] + (pos[v] − pos[entry[u]]) mod length |
| on a cycle, a different cycle | unreachable |
| on a tail, u passes through it | dist[u] − dist[v] |
| on a tail, u does not pass through it | unreachable |
1// answers "how many steps from u until v?" in O(distance to v) after an O(n) setup2static class Functional {3final int[] next, dist, entry, pos, cycleLen, cycleId; // pos, cycleLen, cycleId: meaningful for cycle vertices4Functional(int[] next) {5this.next = next;6int n = next.length;7boolean[] onCycle = new boolean[n];8int[] state = new int[n];9for (int s = 0; s < n; s++) { // find the cycles (3-colour walk)10int v = s;11while (state[v] == 0) { state[v] = 1; v = next[v]; }12if (state[v] == 1) for (int u = v; ; u = next[u]) { onCycle[u] = true; if (next[u] == v) break; }13for (int u = s; state[u] == 1; u = next[u]) state[u] = 2;14}15dist = new int[n]; entry = new int[n]; pos = new int[n]; cycleLen = new int[n]; cycleId = new int[n];16boolean[] done = new boolean[n];17for (int s = 0; s < n; s++) { // number every cycle: position, length and an id18if (!onCycle[s] || done[s]) continue;19int len = 0;20for (int v = s; !done[v]; v = next[v]) { done[v] = true; pos[v] = len++; cycleId[v] = s; entry[v] = v; }21for (int v = s, i = 0; i < len; v = next[v], i++) cycleLen[v] = len;22}23for (int s = 0; s < n; s++) { // tail length and landing vertex of every other vertex24Deque<Integer> path = new ArrayDeque<>();25int v = s;26while (!done[v]) { path.push(v); v = next[v]; }27while (!path.isEmpty()) { int u = path.pop(); dist[u] = dist[next[u]] + 1; entry[u] = entry[next[u]]; done[u] = true; }28}29}30int steps(int u, int v) { // fewest steps from u to v, or -131if (dist[v] == 0) { // v is on a cycle: walk the tail, then round the cycle32int e = entry[u];33if (cycleId[e] != cycleId[v]) return -1;34return dist[u] + (pos[v] - pos[e] + cycleLen[v]) % cycleLen[v];35}36int gap = dist[u] - dist[v]; // v is on a tail: u must pass through it37if (gap < 0) return -1;38int x = u;39for (int i = 0; i < gap; i++) x = next[x];40return x == v ? gap : -1;41}42}
Worst
O(n) setup, O(1) to O(n) per query (O(log n) with lifting)
Space
O(n)
Practice ladder
Now try it yourself
Easy
- Happy Number
Medium
- Find the Duplicate Number
- Linked List Cycle II
Advanced
- Maximum Employees to Be Invited to a Meeting
- Longest Cycle in a Graph
Key takeaways
- Every component of a functional graph is one cycle with in-trees; find cycles with a 3-state walk in O(n).
- dist and entry arrays give tail length and landing vertex; cycle arithmetic finishes the distance queries.
- Binary lifting turns k steps into O(log k) lookups.