Функция Аккермана
— всюду определённая
вычислимая функция
, которая не является
примитивно рекурсивной
. Она принимает два неотрицательных
целых числа
в качестве параметров и возвращает
натуральное число
, обозначается
. Эта функция растёт очень быстро, например, число
настолько велико, что количество цифр в порядке этого числа многократно превосходит количество атомов в
наблюдаемой части Вселенной
. В
теоретической информатике
она применяется для демонстрации пределов возможностей
компьютеров
и методов оптимизации. Также существует целое семейство родственных ей функций, имеющих схожую скорость роста и схожее определение.
Содержание
История
В
1926 году
Давид Гильберт
предположил, что каждая
вычислимая функция
примитивно-рекурсивна
. Проще говоря, каждую вычислимую функцию можно построить из базового набора при помощи нескольких очень простых правил, причём продолжительность вычисления может быть оценена заранее. Функции, для которых это неверно, очень редки.
В том же 1926 году
Аккерман
построил функцию, опровергающую эту гипотезу, и опубликовал свой контрпример в 1928 году. Впоследствии эта функция получила название
гипероператора.
Он может быть оценён компьютером за конечное время, но не является примитивно-рекурсивным.
В 1935 году
Рожа Петер
упростила построение Аккермана, получив столь же быстрорастущую функцию. Именно она и стала называться функцией Аккермана.
Построение
Для построения функции используется
последовательность
, в которой каждый следующий оператор определяется так, что предыдущий оператор применяется
раз к числу
. Например, результат
применения второго оператора к числам
и
равен
, где число
входит в выражение
раз. Аналогично значение
третьего оператора равно
. Аккерманн предложил рассмотреть функцию
, возвращающую результат применения
-го оператора последовательности к числам
и
.
Например, применив все операторы последовательности к числам
и
, получатся следующие результаты: 6, 8, 16, 65536,
(с 65536 двойками),… Пятый член последовательности настолько велик, что количество цифр в его десятичной записи намного больше числа атомов во Вселенной.
Таким образом, построенная Аккерманом функция
удовлетворяет следующим уравнениям:
,
,
,
.
Начиная с четвёртой строки функцию становится затруднительно описать с помощью обычных операторов.
Определение и варианты
Функция Аккермана обычно определяется
рекурсивно
, то есть явно задаётся для некоторых начальных значений, после чего описывается алгоритм получения дальнейших значений функции из уже рассчитанных.
Изначально Аккерман определил функцию довольно громоздко, но вскоре дал следующее эквивалентное определение:
,
,
.
Здесь
— ещё одна функция, задающая начальные значения
:
Например, при вычислении
можно применить первую строку определения и получить
. При вычислении
применяется вторая строка:
;
Если ни второй, ни третий аргумент не равен 0, используется третья строка определения, например,
. При подстановке
из вычисления
получается
. Продолжая рекурсивно вычислять значения функции
получается
:
При оценке скорости роста функции Аккермана обычно изучают функцию
.
Определение Петер
В 1935 году
Рожа Петер
предложила более простое построение, в котором функция имеет только два параметра, а вспомогательная функция
не используется
:
Говоря о скорости роста функции Аккермана, тоже имеют в виду скорость роста функции
.
Рекурсия всегда заканчивается. Это следует из того, что при рекурсивном вызове или уменьшается значение
, или значение
сохраняется, но уменьшается значение
. Это означает, что каждый раз пара
уменьшается с точки зрения
лексикографического порядка
, значит, значение
в итоге достигнет нуля: для одного значения
существует конечное число возможных значений
(так как первый вызов с данным
был произведён с каким-то определённым значением
, а в дальнейшем, если значение
сохраняется, значение
может только уменьшаться), а количество возможных значений
, в свою очередь, тоже конечно. Однако, при уменьшении
значение, на которое увеличивается
, неограниченно и обычно очень велико.
Применение в теоретической информатике
Аккерман привёл эту функцию в качестве примера функции, которая не является примитивно-рекурсивной, но является
вычислимой
.
Некоторое время предполагалось, что все вычислимые функции примитивно-рекурсивны, то есть вычислимость любой функции можно доказать, построив алгоритм, или опровергнуть, убедившись, что она не является примитивно-рекурсивной. Однако функция Аккермана, будучи вычислимой, но не примитивно-рекурсивной (доказательство этого приведено ниже), опровергла это предположение.
Если ввести ещё одно правило построения, так называемую
частичную рекурсию
, то класс получающихся таким способом функций расширится и будет, в частности, содержать функцию Аккермана. Предполагается, что этот класс частично рекурсивных функций равен классу вычислимых функций (
тезис Чёрча
).
Доказательство
Чтобы доказать, что функция Аккермана вычислима, но не является примитивно-рекурсивной, мы докажем, что каждая примитивно рекурсивная функция (далее- ПРФ) растёт медленнее функции Аккермана
.
Набросок доказательства утверждения, что функция Аккермана не является примитивно-рекурсивной:
Сначала определим для каждой ПРФ
функцию
Эта функция возвращает наибольшее значение, которое можно получить при помощи функции
, не используя аргументов выше
.
Затем с помощью
индукции по структуре
ПРФ показывается, что для любой ПРФ
существует натуральное число
, такое, что при всех
.
Если бы функция Аккермана
была ПРФ, то функция
сама оказалась бы примитивно рекурсивной, и для некоторого
мы бы получили, что при всех
Однако функция Аккермана монотонна по обоим аргументам. Противоречие.
Приложения
Существует очень мало приложений для функции Аккермана. Помимо невероятной глубины рекурсии, из-за которой она часто применяется в тестах производительности рекурсивных вызовов в языках программирования, функция Аккермана используется для оценки времени выполнения взвешенного объединения и сжатия пути в
системе непересекающихся множеств
.
В качестве эталона для проверки рекурсивных вызовов
процедур
часто используется функция Аккермана, так как непосредственное вычисление этой функции состоит почти только из них. В определении Петер непосредственно заданы только значения
. Все остальные значения вычисляются с помощью многократных глубоко вложенных вызовов, что легко может привести к
переполнению стека
, указывающему на то, что системе не хватает памяти. Таким образом, функция Аккермана является простым и безопасным методом провоцирования
переполнения стека
, например, для проверки того, обрабатывается ли этот случай ошибки и, если да, то как это делается. Преимущество функции Аккермана в том, что она невосприимчива к оптимизации компилятора, а статический анализ исходного кода практически не способен обнаружить (возможное) переполнение стека.
Эта идея восходит к Ингве Сундбладу, который в 1971 году начал использовать функцию
для сравнения различных языков программирования. При вычислении
происходит около
вложенных вызовов.
Компания Sundblad проверила, среди прочего, максимальное значение
, при котором вычисление
не вызывает переполнения. В то время максимально возможное значение
было равно 1. Сейчас в языке
Java
1.4.2 со стандартными настройками памяти максимальное допустимое значение
.
В ходе расчета множество одинаковых вызовов обсчитываются несколько раз. Умный компилятор может воспользоваться этим и кэшировать результаты, чтобы не вычислять одно и то же значение много раз. Уже в 1971 году таким способом было достигнуто число
. Другим способом оптимизации является вычисление
напрямую без рекурсивного раскрытия в
. Прямой расчёт
требует линейного времени по
. Расчёт
требует квадратичного времени, потому что при этом происходят
вложенных вызовов
для разных
. Расчёт
занимает
времени.
Оценки времени выполнения, содержащие обратную функцию Аккермана
Поскольку функция
растёт очень быстро, её
обратная функция
растет очень медленно. Поскольку для всех практически встречающихся значений
значение
меньше 5, при практическом анализе алгоритмов его можно считать постоянным.
Реализация
В псевдокоде функция Аккермана реализуется по определению:
функция ack(n, m)
если n = 0
вернуть m + 1
иначе, если m = 0
вернуть ack (n - 1, 1)
ещевернуть ack(n - 1, ack (n, m - 1))
Следующая частично итерационная реализация несколько более эффективна:
функция ack(n, m)
пока n ≠ 0
если m = 0
m:= 1
иначе
m:= ack(n, m - 1)
n:= n - 1
вернуть m + 1
ackermann(0,X,Y) :- X >= 0,!, Y = X + 1.
ackermann(X,0,Z):- X > 0,!, X1 = X - 1, ackermann(X1,1,Z).
ackermann(X,Y,Z):- X > 0, Y > 0, X1 = X-1, Y1 = Y-1, ackermann(X,Y1,W), ackermann(X1,W,Z).
Хотя в этой таблице возникают невообразимо большие числа, впоследствии были описаны рекуррентные построения, порождающие ещё бо́льшие числа, такие как
число Грэма
.
Память, требуемая для хранения
как целого числа без знака.
Используя таблицу значений, можно вывести схему вычисления значений функции, которую легче понять, чем формальное рекурсивное определение. Легко заметить, что значения в первой строке — это просто список всех натуральных чисел:
. Все последующие строки просто содержат инструкции для поиска значения в этой строке. При
легко показать, что
Теперь рассмотрим более сложный случай вычисления функции
, значение которой настолько велико, что записать его в десятичном виде невозможно.
Вычислить напрямую подобные значения совершенно невозможно. Даже очень простые на вид выражения Аккермана практически не поддаются вычислению. Каждая строка в предыдущем примере — это отдельное применение одной из трёх частей определения функции Аккермана.
Ещё одним аспектом функции Аккермана является то, что единственное вычисление, которое действительно появляется помимо рекурсивных вызовов, — это вычисление
Литература
Dexter C. Kozen:
The Design and Analysis of Algorithms
. Springer, Berlin 1992,
ISBN 3-540-97687-6
.
Yngve Sundblad:
The Ackermann Function. A Theoretical, Computational, and Formula Manipulative Study.
In:
BIT — numerical mathematics.
Springer, Dordrecht 11.1971, S. 107—119,
ISSN
.