🧠 COMPUTER SCIENCE

Черепаха и заяц: как поймать цикл, имея память всего на два указателя

Как понять, что вы ходите по кругу, если нельзя ничего записывать и запоминать пройденное? Роберт Флойд предложил пустить вдогонку двух бегунов с разной скоростью — и быстрый неизбежно нагонит медленного.

Чтобы поймать зацикливание, не нужна таблица посещённых мест — достаточно двух бегунов с разной скоростью на одной дорожке.
Идею приписывают Роберту Флойду; за элегантность её прозвали «алгоритм черепахи и зайца» в честь басни Эзопа.

Задача: есть ли петля

Представьте односвязный список — цепочку узлов, где каждый ссылается на следующий. Обычно она заканчивается «ничем». Но что, если где-то ссылка ведёт назад, образуя петлю? Тогда обход никогда не завершится — вы будете вечно бегать по кругу. Как это обнаружить? Та же задача возникает для последовательностей вида $x_{n+1} = f(x_n)$: рано или поздно значения зацикливаются, и надо найти период.

Наивное решение и его цена

Очевидный путь — запоминать все посещённые узлы в множестве. Дошли до узла, которого ещё не видели, — запомнили; встретили знакомый — значит, цикл. Работает, но требует памяти на каждый узел — $O(n)$. А если узлов миллиарды или это бесконечная последовательность? Памяти не хватит. Флойд показал, как обойтись двумя указателями — $O(1)$ памяти, сколько бы ни было узлов.

Идея: два бегуна, разная скорость

Пускаем по списку двух «бегунов» из одной точки. Черепаха делает один шаг за такт, заяц — два. Если список конечен и без петли, заяц добежит до конца — цикла нет. А если петля есть, заяц в неё войдёт первым и начнёт нарезать круги. Когда черепаха тоже войдёт в петлю, заяц уже там — и с каждым тактом дистанция между ними внутри кольца сокращается ровно на единицу (заяц приближается на 2, черепаха уходит на 1, итог −1). Раз разрыв уменьшается на целое число каждый такт, он неминуемо обнулится: указатели встретятся в одной и той же точке. Встретились — значит, цикл точно есть.

def has_cycle(start, succ):
    slow = fast = start
    while True:
        slow = succ(slow)          # черепаха: 1 шаг
        fast = succ(succ(fast))    # заяц: 2 шага
        if fast is None:           # заяц упёрся в конец — цикла нет
            return False
        if slow == fast:           # встретились внутри петли
            return True

# Последовательность с зацикливанием: f(x) = (x*x + 1) mod 255
def f(x):
    return (x * x + 1) % 255

print(has_cycle(3, f))   # True — рано или поздно зациклится

# Поиск начала цикла (вторая фаза Флойда)
def cycle_start(start, succ):
    slow = succ(start)
    fast = succ(succ(start))
    while slow != fast:            # фаза 1: найти точку встречи
        slow = succ(slow)
        fast = succ(succ(fast))
    slow = start                  # фаза 2: один из начала, шаг в шаг
    while slow != fast:
        slow = succ(slow)
        fast = succ(fast)
    return slow                   # это и есть вход в петлю

print("Цикл начинается в:", cycle_start(0, f))

Бонус: где петля начинается

Флойд придумал и вторую фазу, чтобы найти вход в цикл, а не просто факт его наличия. Магия в числах: пусть до входа в петлю $\mu$ шагов, а длина петли $\lambda$. К моменту встречи черепаха прошла $\mu + k$ шагов внутри петли, заяц — вдвое больше; разность их путей кратна $\lambda$. Из этого следует красивое: если теперь поставить одного бегуна обратно в начало, а другого оставить в точке встречи и двигать обоих по одному шагу, они встретятся ровно во входе в петлю.

$$\text{путь зайца} = 2 \cdot \text{путь черепахи} \;\Rightarrow\; \text{расстояние от старта до входа} \equiv \text{от встречи до входа} \pmod{\lambda}$$

ПодходПамятьВремя
Множество посещённых$O(n)$$O(n)$
Черепаха и заяц$O(1)$$O(n)$

Где это применяют

Поиск цикла в связных структурах нужен сборщикам мусора (обнаружение циклических ссылок), детекторам зависаний и зацикливаний, а в криптографии и теории чисел тот же приём (под именем «ро-алгоритм Полларда») используют для факторизации больших чисел и поиска коллизий хеш-функций. Везде, где последовательность может зациклиться, а памяти на лог посещений нет, выходят черепаха и заяц.

Мораль

Алгоритм Флойда — урок о том, что иногда вместо «запомнить всё» достаточно «двигаться с умом». Два указателя и разные скорости заменяют целую таблицу истории. Это та самая инженерная красота, когда жёсткое ограничение по памяти не ломает задачу, а подсказывает неожиданно изящное решение.

#алгоритмы#поиск цикла#структуры данных#указатели#Флойд