diff --git a/searches/floyds_cycle_finding.py b/searches/floyds_cycle_finding.py new file mode 100644 index 000000000000..f9c62ef6d488 --- /dev/null +++ b/searches/floyds_cycle_finding.py @@ -0,0 +1,91 @@ +""" +An implementation of Floyd's Cycle-Finding Algorithm. +Also known as the "tortoise and the hare" algorithm. + +This algorithm is used to detect a cycle in a sequence of iterated function values. +It can also find the starting index and length of the cycle. + +Wikipedia: https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare +""" + +from collections.abc import Callable +from typing import Any + + +def floyds_cycle_finding( + successor_function: Callable[[Any], Any], start_value: Any +) -> tuple[int, int] | None: + """ + Finds a cycle in the sequence of values generated by the successor function. + + Args: + successor_function: A function that takes a value and returns the next value. + start_value: The starting value of the sequence. + + Returns: + A tuple containing the index of the first element of the cycle (mu) + and the length of the cycle (lam), or None if no cycle is found. + + Doctest examples: + >>> # Example with a mathematical sequence + >>> sequence_func = lambda x: (2 * x + 3) % 17 + >>> floyds_cycle_finding(sequence_func, 0) + (0, 8) + + >>> # Example with a list acting as a sequence + >>> get_next_item = lambda x: [1, 2, 3, 4, 5, 3][x] + >>> floyds_cycle_finding(get_next_item, 0) + (2, 3) + + >>> # Example with a graph-like structure (no cycle) + >>> get_next_node = lambda x: {0: 1, 1: 2, 2: 3, 3: None}.get(x) + >>> floyds_cycle_finding(get_next_node, 0) + + """ + # Phase 1: Find a repetition x_i = x_2i. + # The tortoise moves one step at a time, and the hare moves two. + tortoise = start_value + hare = successor_function(start_value) + + # The hare moves twice as fast as the tortoise. + # The loop continues as long as they are not at the same value + # and the hare has not reached the end of the sequence. + while tortoise != hare: + if hare is None: + return None + tortoise = successor_function(tortoise) + + # Move hare two steps, with a check after each step. + hare = successor_function(hare) + if hare is None: + return None + hare = successor_function(hare) + + # If the loop exits, a cycle was found. + + # Phase 2: Find the position of the first repetition (mu). + # Reset tortoise to the start and move both one step at a time until they + # meet again. + mu = 0 + tortoise = start_value + while tortoise != hare: + tortoise = successor_function(tortoise) + hare = successor_function(hare) + mu += 1 + + # Phase 3: Find the length of the cycle (lam). + # Fix the tortoise at the start of the cycle and move the hare + # until it returns to the tortoise. + lam = 1 + hare = successor_function(tortoise) + while tortoise != hare: + hare = successor_function(hare) + lam += 1 + + return mu, lam + + +if __name__ == "__main__": + import doctest + + doctest.testmod()