Виявлення циклуВ інформатиці виявлення циклу — це алгоритмічна задача пошуку циклу в послідовності ітераційних значень функції. Для будь-якої функції f, яка відображає скінченну множину S на саму себе, і будь-якого початкового значення x0 з S, послідовність ітераційних значень функції зрештою буде двічі використовувати одне й те ж значення, тобто знайдеться пара різних індексів i та j таких, що xi = xj. Як тільки це трапиться, послідовність буде періодично продовжуватися, повторюючи ту саму послідовність значень від xi до xj − 1. Виявлення циклу — це задача пошуку i та j для заданих f та x0. Відомо кілька алгоритмів швидкого знаходження циклів з незначним використанням пам'яті. Алгоритм черепахи та зайця Роберта У. Флойда переміщує два вказівника з різною швидкістю по послідовності значень, поки обидва не вкажуть на рівні значення. Алгоритм Брента базується на ідеї експоненціального пошуку[en]. Алгоритми Флойда і Брента використовують сталий об'єм пам'яті, а кількість разів обчислення значеннь функції пропорційна відстані від початку послідовності до першого повторення. Кілька інших алгоритмів використовують більший об'єм пам'яті для меншої кількості обчислень значень функції. Серед застосунків виявлення циклів є перевірка якості генераторів псевдовипадкових чисел та криптографічних хеш-функцій, алгоритмів обчислювальної теорії чисел, виявлення нескінченних циклів у комп'ютерних програмах та періодичних конфігурацій у клітинних автоматах, автоматизований аналіз форми[en] в таких структурах даних, як зв'язаний список, взаємне блокування при обробці транзакцій в СУБД. ПрикладНа малюнку показана функція f, яка відображає множину S={0,1,2,3,4,5,6,7,8} саму до себе. Якщо починати з x0 = 2 і послідовно знаходити f, отримаємо послідовність значень функції: 2, 0, 6, 3, 1, 6, 3, 1, 6, 3, 1, ... Циклом у цій послідовності буде підпослідовність 6, 3, 1. ВизначенняНехай S — будь-яка скінченна множина, f — будь-яка функція, що відображає S на саму себе, — будь-який елемент з S. Для будь-якого нехай . Нехай μ — найменший індекс, для якого значення xμ знову з'являється нескінченно часто в межах послідовності значень xi, а λ (довжина петлі) — це найменше натуральне число, що xμ = xλ + μ. Проблема виявлення циклу — це задача пошуку λ і μ.[1] Цю ж задачу розглянемо с точки зору теорії графів: побудуємо орієнтований граф (у якому кожна вершина має єдине вихідне ребро), якому відповідає граф функції нашої задачі, та вершинами якого є елементи множини S, а ребра якого відображають елемент на відповідне значення функції, як показано на малюнку. Набір вершин доступних від початкової вершини x0 утворюють підграф форми, що нагадує грецьку букву rho (ρ): шлях довжини μ від x0 до цикла, який складається з λ вершин.[2] Комп'ютерне представленняЯк правило, f не буде вказано таблицею значень, як це показано на малюнку вище. Швидше за все, алгоритму виявлення циклу може бути наданий доступ або до послідовності значень xi, або до підпрограми для обчислення f. Завдання полягає в тому, щоб знайти λ і μ, перевіряючи якомога менше значень з послідовності або виконуючи якомога менше викликів підпрограм. Як правило, також важлива просторова складність алгоритму для проблеми виявлення циклу: бажано вирішити цю проблему, використовуючи обсяг пам'яті, значно менший, ніж потрібно для збереження всієї послідовності. У деяких додатках, і зокрема в ρ-алгоритм Полларда для факторізації цілих чисел, алгоритм має набагато обмежений доступ до S та f. Наприклад, в ρ-алгоритмі Полларда S — це набір цілих чисел за модулем невідомого простого множника числа, що підлягає факторизації, тому навіть розмір S невідомий алгоритму. Щоб дозволити алгоритмам виявлення циклу використовувати такі обмежені знання, вони можуть бути розроблені на основі наступних можливостей. Спочатку передбачається, що алгоритм має у своїй пам'яті об'єкт, що представляє вказівник на початкове значення x0. На будь-якому етапі він може виконати одну з трьох дій: він може скопіювати будь-який вказівник, який він має, на інший об'єкт у пам'яті, він може застосувати f і замінити будь-який з його покажчиків на вказівник на наступний об'єкт у послідовності, або він може застосувати підпрограма для визначення того, чи є два її покажчики рівними значеннями в послідовності. Дія тестування рівності може включати деякі нетривіальні обчислення: наприклад, в ρ-алгоритмі Полларда це реалізується шляхом перевірки того, чи має різниця між двома збереженими значеннями нетривіальний найбільший спільний дільник з числом, яке потрібно врахувати.[2] У цьому контексті, за аналогією до моделі обчислень покажчикової машини[en], алгоритм, який використовує лише копіювання покажчика, просування в межах послідовності та тести рівності, можна назвати алгоритмом покажчика. АлгоритмиЯкщо вхід подається як підпрограма для обчислення f, задачу виявлення циклу можна тривіально вирішити, використовуючи лише λ + μ, просто обчисливши послідовність значень xi та використовуючи структуру даних, таку як хеш-таблицю для їх зберігання значення та перевірити, чи кожне наступне значення вже збережено. Проте просторова складність цього алгоритму пропорційна λ + μ, надмірно велика. Крім того, для реалізації цього методу як алгоритму вказівників[en] знадобиться застосувати тест рівності до кожної пари значень, що призведе до загального квадратичного часу. Таким чином, дослідження в цій галузі зосереджені на двох цілях: використати менше місця, ніж цей наївний алгоритм, і знайти алгоритми покажчиків, які використовують менше тестів рівності. Черепаха і заєць ФлойдаАлгоритм пошуку циклу Флойда — це алгоритм покажчиків, який використовує лише два покажчики, які рухаються по послідовності з різною швидкістю. Його також називають «алгоритмом черепахи та зайця», натякаючи на байку Езопа про «Черепаху та зайця». Алгоритм названий на честь Роберта У. Флойда, якому його винахід приписував Дональд Кнут.[3][4] Однак алгоритм не з'являється у опублікованій роботі Флойда, і це може бути хибним віднесенням: Флойд описує алгоритми перерахування всіх простих циклів в орієнтованому графі у роботі 1967 року[5] але ця робота не описує проблему пошуку циклу у графах функцій, що є темою цієї статті. Насправді, твердження Кнута (1969 року), яке він приписує Флойду без цитування, є першим відомим згадуванням у друкованому вигляді, і, отже, він може належати до математичного фольклору[en] і не належати окремій особі.[6] Ключове розуміння алгоритму наступне. Якщо існує цикл, то для будь-яких цілих чисел i ≥ μ і k ≥ 0, xi = xi + kλ, де λ — довжина петлі, яку потрібно знайти, а μ — індекс першого елемента циклу. Виходячи з цього, тоді можна показати, що i = kλ ≥ μ для деякого k тоді і тільки тоді, коли xi = x2i. Таким чином, алгоритму потрібно лише перевірити наявність повторюваних значень цієї спеціальної форми, одна вдвічі далі від початку послідовності, ніж інша, щоб знайти період ν повторення, кратний λ. Як тільки ν буде знайдено, алгоритм відновлює послідовність від її початку, щоб знайти перше повторене значення xμ у послідовності, використовуючи той факт, що λ ділить ν і, отже, xμ = xμ + v. Нарешті, як тільки значення μ стало відомим, тривіально знайти довжину λ найкоротшого повторюваного циклу шляхом пошуку першого положення μ + λ для якого xμ + λ = xμ. Таким чином, алгоритм утримує два вказівники у даній послідовності: один (черепаха) у точці xi, а інший (заєць) у точці x2i. На кожному кроці алгоритму він збільшується i на один, рухаючи черепаху на один крок вперед, а зайця на два кроки вперед у послідовності, а потім порівнює значення послідовності за цими двома вказівниками. Найменше значення i > 0 для якого черепаха та заєць вказують на рівні значення, є бажаним значенням ν. Наступний код Python показує, як ця ідея може бути реалізована як алгоритм. def floyd(f, x0):
# Основна фаза алгоритму: пошук повторень x_i = x_2i.
# Заєць рухається вдвічі швидше черепахи та
# відстань між ними збільшується на 1 на кожному кроці.
# Врешті -решт вони обидва опиняться всередині циклу, а потім,
# в певний момент відстань між ними буде
# ділиться на крапку λ.
tortoise = f(x0) # f (x0) - елемент/вузол поруч із x0.
hare = f (f (x0))
while tortoise != hare:
tortoise = f(tortoise)
hare = f(f(hare))
# На цьому місці розташування черепахи ν, що також дорівнює
# на відстань між зайцем і черепахою ділиться на
# період λ. Тож зайці рухаються по колу крок за кроком,
# і черепаха (скинути на x0) рухаються до кола, буде
# перетинаються на початку кола. Тому що
# відстань між ними є постійною на 2ν, кратною λ,
# вони погодяться, як тільки черепаха досягне індексу μ.
# Знайдіть позицію μ першого повторення.
mu = 0
tortoise = x0
while tortoise != hare:
tortoise = f(tortoise)
hare = f(hare) # Заєць і черепаха рухаються з однаковою швидкістюd
mu += 1
# Знайдіть довжину найкоротшого циклу, починаючи з x_μ
# Заєць рухається крок за кроком, поки черепаха нерухома.
# lam збільшується, поки не знайдеться λ.
lam = 1
hare = f(tortoise)
while tortoise != hare:
hare = f(hare)
lam += 1
return lam, mu
Цей код звертається лише до послідовності шляхом зберігання та копіювання покажчиків, оцінок функцій та тестів рівності; тому він кваліфікується як алгоритм вказівників. Алгоритм використовує O(λ + μ) цих типів та O(1) простір для зберігання.[7] Алгоритм БрентаРічард П. Брент[en] описав альтернативний алгоритм виявлення циклу, який, як і алгоритм черепахи та зайця, вимагає лише двох вказівників у послідовності.[8] Однак він базується на іншому принципі: пошук найменшої потужності з двох 2i, більшої як за λ і за μ. Для i = 0, 1, 2, ..., алгоритм порівнює x2i−1 з кожним наступним значенням послідовності до наступного степеня двох, зупиняючись, коли знаходить відповідність. Він має дві переваги порівняно з алгоритмом черепахи та зайця: він безпосередньо знаходить правильну довжину λ циклу, замість того, щоб шукати його на наступному етапі, а його кроки передбачають лише одну оцінку f, а не три.[9] Наступний код Python більш детально показує, як працює ця техніка. def brent(f, x0):
# основний етап: пошук степеня двійки
power = lam = 1
tortoise = x0
hare = f(x0) # f (x0) - елемент/вузол поруч із x0.
while tortoise != hare:
if power == lam: # час почати нову степінь двійки?
tortoise = hare
power *= 2
lam = 0
hare = f(hare)
lam += 1
# Знайдіть позицію першого повторення довжини λ
tortoise = hare = x0
for i in range(lam):
# range(lam) створює список зі значеннями 0, 1, ..., lam-1
hare = f(hare)
# Відстань між зайцем і черепахою тепер λ.
# Далі заєць і черепаха рухаються з однаковою швидкістю, поки вони не домовляться
mu = 0
while tortoise != hare:
tortoise = f(tortoise)
hare = f(hare)
mu += 1
return lam, mu
Як і алгоритм черепахи та зайця, це алгоритм вказівників, який використовує O(λ + μ) та оцінки функцій та O(1) простір для зберігання. Не важко показати, що кількість оцінок функцій ніколи не може бути вище, ніж для алгоритму Флойда. Брент стверджує, що в середньому його алгоритм пошуку циклу працює приблизно на 36 % швидше, ніж алгоритм Флойда, і що він швидше ρ-алгоритма Полларда приблизно на 24 %. Він також виконує аналіз середніх випадків[en] для рандомізованої версії алгоритму, в якому послідовність індексів, відстежена повільнішим із двох покажчиків, — це не повноваження двох самих, а скоріше рандомізоване кратне ступеням двох. Хоча його основне призначення — алгоритми цілочисельної факторизації, Брент також обговорює застосування для тестування генераторів псевдовипадкових чисел.[8] Алгоритм ГоспераАлгоритм Р.В. Госпера[en][10][11] знаходить період і нижню і верхню межу вихідної точки першого циклу. Різниця між нижньою та верхньою межею має той самий порядок, що й період, наприклад. . Головною особливістю алгоритму Госпера є те, що він ніколи не створює резервних копій для переоцінки функції генератора, і є економічним як у просторі, так і в часі. Його можна приблизно охарактеризувати як паралельну версію алгоритму Брента. У той час як алгоритм Брента поступово збільшує розрив між черепахою та зайцем, алгоритм Госпера використовує кілька черепах (збережено кілька попередніх значень), які приблизно експоненціально рознесені. Відповідно до примітки в пункті 132 HAKMEM [Архівовано 15 вересня 2020 у Wayback Machine.], цей алгоритм буде виявляти повторення до третього входження будь-якого значення, наприклад. цикл повторюватиметься максимум двічі. У цій примітці також зазначено, що її достатньо для зберігання попередні значення; однак, передбачена реалізація[10] зберігає цінності. Наприклад: значення функції-це 32-розрядні цілі числа, і апріорі відомо, що друга ітерація циклу закінчується після щонайменше 232 оцінок функції з початку, тобто. . Тоді достатньо зберегти 33 32-розрядні цілі числа. На -го оцінювання функції генератора, алгоритм порівнює отримане значення з попередні значення; зауважте це піднімається принаймні і максимум . Тому складність цього алгоритму у часі дорівнює . Так як він зберігається цінностей, її просторова складність становить . Це за звичайного припущення, наявного у цій статті, що розмір значень функції постійний. Без цього припущення складність простору така оскільки нам принаймні потрібно різні значення, а отже, розмір кожного значення . Компроміси між часом і просторомРяд авторів вивчали методи виявлення циклів, які використовують більше пам'яті, ніж методи Флойда та Брента, але виявляють цикли швидше. Загалом ці методи зберігають декілька попередньо обчислених значень послідовності та перевіряють, чи кожне нове значення дорівнює одному з попередньо обчислених значень. Для того, щоб зробити це швидко, вони зазвичай використовують хеш-таблицю або подібну структуру даних для зберігання попередньо обчислених значень, і тому не є алгоритмами покажчиків: зокрема, вони зазвичай не можуть бути застосовані до ρ-алгоритма Полларда. Ці методи відрізняються тим, як вони визначають, які значення зберігати. Слідом за Нівашем[12] ми коротко оглядаємо ці методи.
Будь-який алгоритм виявлення циклу, який зберігає не більше M значень із вхідної послідовності, повинен виконувати принаймні оцінки функцій.[18][19] ЗастосуванняВиявлення циклів має багато застосувань.
Примітки
Посилання
|