Исходный вариант статьи (В. В. Пупышев, «Рекурсия: плохо или хорошо?») опубликован в журнале «Потенциал».

Рекурсия — это жемчужина теории алгоритмов, и это первое, с чем знакомят школьников (сразу после процедур ввода и вывода данных, элементарных арифметических операций, оператора цикла и условного оператора).

Простота рекурсии обманчива. Метод рекурсии таит в себе много опасностей и сложностей, и в то же время готовит много приятных сюрпризов.

Давно известен такой математический приём, как разбиение задачи на простые шаги, каждый из которых тоже можно разложить на более мелкие шаги и так далее, пока не доберёмся до самых элементарных «шажочков».

Представим, что нужно пройти 1000 шагов. Для решения делаем один шаг, остаётся 999: задача упростилась. Сделав такое упрощение 999 раз, дойдём до самой элементарной задачи — шагнуть один раз. Конечно, этот пример слишком прост. Далее мы рассмотрим более сложные примеры, освещающие явление рекурсии как с хорошей так, и с плохой стороны.

Вы, наверное, уже заметили сходство понятий рекурсии и математической индукции. У рекурсии, как и у математической индукции, есть база — аргументы, для которых значения функции определены (элементарные задачи), и шаг рекурсии — способ сведения задачи к более простым.

Числа Фибоначчи

править

Рассмотрим последовательность чисел   в которой каждое число является суммой двух предыдущих. Это числа Фибоначчи. Формальное их определение таково:

 ,  ,  , если  .

Функция   задана рекурсивно, то есть «через себя». База — значения функции   на аргументах 1 и 2, а шаг — формула  .

Современные языки программирования дают возможность программировать рекурсивные определения без особых усилий, но в таких определениях таятся опасности.

Проблемы рекурсии и как их решать

править

При вычислении значения   будут вызваны процедуры вычисления   и  . В свою очередь, для вычисления последних потребуется вычисление двух пар  ,   и  ,  . Можно нарисовать «дерево рекурсивных вызовов».

 

Можно заметить, что   вычисляется три раза. Если рассмотреть вычисление   при больших  , то повторных вычислений будет очень много. Это и есть основной недостаток рекурсии — повторные вычисления одних и тех же значений. Кроме того, с рекурсивными функциями связана одна серьезная ошибка: дерево рекурсивных вызовов может оказаться бесконечным и компьютер «зависнет». Важно, чтобы процесс сведения задачи к более простым когда-нибудь заканчивался.

Есть способ решить проблему повторных вычислений. Он очевиден — нужно запоминать найденные значения, чтобы не вычислять их каждый раз заново. Конечно, для этого придётся активно использовать память.

 

Например, рекурсивный алгоритм вычисления чисел Фибоначчи легко дополнить тремя «строчками»:

  1. создать глобальный массив  , состоящий из нулей;
  2. после вычисления числа Фибоначчи   поместить его значение в  ;
  3. в начале рекурсивной процедуры сделать проверку на то, что   и, если  , то вернуть   в качестве результата, а иначе приступить к рекурсивному вычислению  .

Такая рекурсия с запоминанием называется динамическим программированием сверху.

Рекурсию с запоминанием для вычисления чисел Фибоначчи мы привели просто для демонстрации идеи. Для чисел Фибоначчи есть простой «человеческий алгоритм», не использующий рекурсивные вызовы и запоминание всех вычисленных значений. Достаточно помнить два последних числа Фибоначчи, чтобы вычислить следующее. Затем предпредыдущее можно «забыть» и перейти к вычислению следующего:

  1.  ;
  2. если  , то сделать   раз:  ;  ;  ;
  3. вернуть ответ  ;

Этот алгоритм линейный по  , то есть для вычисления  -го числа Фибоначчи требуется   шагов. Но здесь есть важная тонкость: число знаков в числе Фибоначчи растёт с  , соответственно, время выполнения операции сложения   тоже увеличивается. А именно, число знаков в числе Фибоначчи растёт примерно линейно (в любой системе счисления). Это следует из явной формулы:

 .

Конечно, пока числа не выходят за пределы машинной точности (на компьютерах с 32-битной архитектурой это означает «меньше  »), сложение выполняется за фиксированое число тактов. Но, начиная с  , уже нельзя использовать элементарные 32-битные целочисленные типы и нужно использовать 64-битные или писать «свою» длинную арифметику, то есть представлять числа в виде массивов цифр в некоторой системе счисления (обычно используют систему с основанием 10000) и писать процедуры сложения таких чисел «столбиком». Например, число десятичных знаков в 1000-м числе Фибоначчи равно 209, и для сложения   «столбиком» в десятичной системе счисления потребуется примерно 209 элементарных операций. Определение сложности задачи вычисления   при больших   довольно трудная задача.

Указанный пошаговый алгоритм вычисления чисел Фибоначчи с учётом затрат на сложения длинных чисел является квадратичным по   (при увеличении   в   раз время вычисления   увеличивается в   раз), а не линейным как многие считают. Числа Фибоначчи в принципе нельзя подсчитать быстрее, чем за линейное время, так как, чтобы вывести цифры числа Фибоначчи  , уже требуется линейное по   время.

Особенно просто и наглядно функцию вычисления чисел Фибоначчи можно задать на языке Mathematica (см. http://www.wolfram.com):

Простое рекурсивное определение: F(n_) := F(n-1) + F(n-2); F(1) = F(2) = 1;

Рекурсивное определение с запоминанием: F[n_] := (F[n] = F[n-1] + F[n-2]); F[1] = F[2] = 1;

Если определить числа Фибоначчи первым способом, то время вычисления   будет более минуты. Если же использовать второе определение, то 209-значное число   будет вычислено практически мгновенно, хотя, безусловно, и второй способ далеко не самый оптимальный.

Задача 1

править

Покажите, что в дереве рекурсивных вызовов рекурсивной функции вычисления числа Фибоначчи   присутствует   вызовов   и   вызовов  . В частности при вычислении   в дереве присутствуют 5 вызовов   и 3 вызова  .

Задача 2

править

Покажите, что рекурсивный алгоритм вычисления числа Фибоначчи   требует времени пропорционального  .

Задача 3

править

Найдите две геометрические прогрессии с различными ненулевыми значениями разности  , удовлетворяющие соотношению  . Покажите, что сумма этих прогрессий также удовлетворяет этому соотношению. Представьте   в виде суммы двух геометрических прогрессий, а именно, найдите  ,  ,  ,   такие, что  .

Задача 4

править

Напишите рекурсивную процедуру вычисления чисел Фибоначчи, основанную на рекуррентных формулах:   и  . Убедитесь в правильности этих формул с помощью численного эксперимента. Нарисуйте дерево рекурсивных вызовов для   и  . Как растёт размер этого дерева в зависимости от  ? Выведите формулы зависимостей   и   от   и  . Напишите рекурсивную функцию, использующую эти четыре формулы в зависимости от чётности   так, чтобы в дереве рекурсивных вызовов были только такие  , для которых двоичная запись   есть левая часть двоичной записи числа  , дополненая справа нулём или единицей.

Задача о золотой горе

править

На международной олимпиаде по информатике в 1994 году в первый день среди прочих задач была дана следующая задача.

 

Формулировка задачи: На рисунке показан пример треугольника из чисел. Написать программу, вычисляющую наибольшую сумму чисел, через которые проходит путь, начинающийся на вершине и заканчивающийся где-то на основании.

  1. Каждый шаг может идти диагонально вниз направо или диагонально вниз налево.
  2. Количество строк в треугольнике  , но  .
  3. Числа в треугольнике все целые от 0 до 99 включительно.

В примере, описанном выше, это путь 7, 3, 8, 7, 5, дающий максимальную сумму 30.

Входные данные
Информация о количестве строк в треугольнике это первое число в файле INPUT.TXT, далее записана информация о треугольнике построчно.
Выходные данные
Наибольшая сумма, записанная как целое число в файл OUTPUT.TXT. Для нашего примера это будет число 30.

В примере, описанном выше, это путь 7, 3, 8, 7, 5, дающий максимальную сумму 30.

Пример входных данных:

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

Эту задачу можно встретить и под названием «Золотая гора» — нужно спуститься с горы и собрать как можно больше золота.

Можно заметить, что в любом месте горы начинается гора меньшего размера, будем называть такие горы горками. Пусть число   есть число в треугольнике, находящееся в  -й строчке на  -м месте, а число   есть максимальное значение суммы, которое можно получить спускаясь с горки, начиная с этого элемента. Понятно, что   есть число   плюс максимум из двух вариантов:   и  . Эти два варианта соответствуют тому, что мы можем спуститься вниз-вправо или вниз-влево. Получилось рекурсивное определение:  . На основе этой рекуррентной формулы можно написать рекурсивный алгоритм. Но нужно быть осторожным. Попробуем вычислить время работы нашего алгоритма при входных данных максимального размера — 100 строк. Рекурсивный алгоритм вычисления функции   перебирает все возможные пути. Сколько их? На каждом шаге есть возможность выбрать один вариант из двух (направо или налево). Количество шагов равно количеству строк треугольника. Значит, количество всех путей равно  (Покажите, что число путей из вершины до  -го элемента  -й строчки равно биномиальному коэффициенту  ) Это очень большое число. Столько вариантов не успеет перебрать даже самая мощная вычислительная техника. Если предположить, что машина просматривает миллиард вариантов в секунду, то понадобится   секунд. Чтобы считать было удобнее, заметим, что   секунд, что соответствует более, чем   часов, или более   лет. Понятно, что такая программа никому не нужна, её результаты узнать невозможно. За такое время все компьютеры, решающие эту задачу, превратятся в пыль.

 

На рисунке обозначены две горки (треугольники): одна с вершиной в числе 3, другая с вершиной в числе 8. Эти горки пересекаются, и их пересечение тоже горка с вершиной в числе 1. Можно заметить, что при вычислении самого лучшего пути по рекурсивному алгоритму горка с вершиной в числе 1 будет использоваться дважды. Для горок с вершинами в нижних строчках повторных вызовов будет ещё больше. Это и есть причина медленности работы алгоритма.

Решить эту задачу за разумное время помогает динамическое программирование. Суть динамического программирования хорошо описана в книге Р. Беллмана [1]. Есть и более современные учебники [2], [3], в них можно найти много интересных алгоритмов, основанных на динамическом программировании.

Общая идея динамического программирования заключается в том, что мы рассматриваем вместо одной задачи целое семейство задач. Это семейство задач у нас будет таким: найти наилучший путь из вершины  . Для каждой пары   получим одну задачу. При   и   получается исходная задача, которую нам нужно решить. Начнём решать эти задачи с простых: сначала для нижней строчки, затем второй снизу и так далее, пока не дойдём до самой верхней строчки треугольника.

По сути, идея динамического программирования заключается в запоминании вычисленных значений  . Их можно рассматривать как двумерный массив, элементы которого пошагово вычисляются с последней строчки, снизу — вверх.

Подсчитаем количество шагов алгоритма, основанного на динамике. Если количество строк  , то длины строк представляют собой последовательные натуральные числа  . Элемент массива вычисляется один раз за небольшое фиксированное количество элементарных операций по формуле  . Значит время работы всего алгоритма пропорционально количеству элементов в нашем треугольнике (а в общем случае — количеству задач в семействе). Ответ равен  . Символ   означает «примерно пропорционально  ». Действительно, если мы   разделим на  , то получим  , что очень близко к константе   (чем больше  , тем ближе).

Задача 5

править

Пусть значения чисел   в треугольнике не хранятся в массиве, а быстро вычисляются некоторой внешней функцией от двух аргументов. Придумайте алгоритм, который использует память   и работает время  .

Подсказка: В памяти достаточно хранить значения   для одной последней строчки — самой верхней из рассмотренных.

Эта задача показывает, что придумать рекурсивный алгоритм часто намного проще, чем не рекурсивный. Также несложно добавить к рекурсии запоминание вычисленных значений. Но нередко существует более быстрый алгоритм, основанный на динамическом программировании, который использует меньше памяти, нежели рекурсия с запоминанием, и делает в два раза меньше операций обращения к памяти.

Задача «Сделай палиндром»

править

Палиндром — это последовательность символов, которая слева-направо и справа-налево пишется одинаково. Например «АБА» или «АББ ББА». Дана последовательность символов. Какое минимальное количество символов нужно удалить из неё, чтобы получить палиндром?

Длина последовательности не больше 20 символов. Ограничение на время работы программы: 5 секунд.

Пример: Вход: ТИТ ЕЛЕ ЛЕТИТ. Выход: 2

Эта задача давалась на районной олимпиаде школьников Удмуртской республики в 1998 году. Рассмотрим её решение, основанное на рекурсии.

Если строка имеет вид  , где   и   символы, а   — подстрока, возможно пустая. Пусть   — вычисляет минимальное количество символов, которые нужно убрать из строки  , чтобы оставшаяся строка была палиндромом.

Базой рекурсии являются строки из одного символа и пустая строка — все они палиндромы по определению, и для них  . Шаг рекурсии состоит из двух частей:

 

Получается, что функция   иногда три раза вызывает себя для меньших строк, чтобы затем из результатов выбрать минимальный. Легко заметить, что тут много повторных вычислений. Избавиться от них поможет запоминание значений   при различных  , которые встречались при вычислении. Однако, тут есть одна техническая проблема. Заметим, что   — это строка. Получается, нужно быстро запоминать и находить значения по заданной строке. Эту задачу решают хэш-таблицы. Но в данном случае можно обойтись и без них. Ясно, что в качестве   могут выступать лишь подстроки исходной строки, то есть некоторые кусочки подряд идущих символов из исходной строки. Каждая подстрока определяется двумя числами — номером первого символа и номером последнего.

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

Алгоритм Евклида

править

Даны два натуральных числа. Найти самое большое натуральное число, которое делит оба без остатка. Такое число называется наибольший общий делитель (НОД) (GCD — Greatest Common Divisor).

Пример: Вход: 18, 600 Выход: 6

Есть очень простой алгоритм: давайте перебирать все числа от минимального из заданных до 1 и проверять, делит ли очередное число оба заданных. Первое такое число и будет НОД. У этого алгоритма есть существенный недостаток — маленькая скорость работы. Например для чисел   и   придётся выполнить   проверок. Более эффективно эту задачу решает алгоритм Евклида, основанный на двух простых свойствах  :

  1.  , где   — натуральное число;
  2. Если  , то  .

Рассмотрим второе свойство. При замене одного из чисел на его разность с первым, наибольший общий делитель остаётся прежним.

Если после вычитания   получается число, большее  , операцию вычитания можно повторить. Так можно продолжать, пока разность не станет меньше  . Можно догадаться, что вместо нескольких вычитаний достаточно выполнить одну операцию деления с остатком и взять получившийся остаток. Запишем формулы, которые позволят нам определить рекурсивную функцию вычисления НОД:

 

Запись « » означает «остаток при делении   на  ». Здесь   — натуральное число.

Оценить количество шагов данного алгоритма достаточно сложно, но скорость его очень велика. Хватит 50 шагов этого алгоритма, чтобы найти НОД любой пары чисел, каждое из которых не превышает  . Интересно отметить, что дольше всего алгоритм работает тогда, когда данные числа есть пара соседних чисел Фибоначчи. Число шагов в алгоритме Евклида напрямую связано с длиной разложения рационального числа   в цепную дробь. Разложением положительного числа   в цепную дробь называется представление  , где числа   натуральные. Разложение дробей (рациональных чисел) в цепную дробь конечно. Вот примеры разложений отношений соседних чисел Фибоначчи в цепную дробь:

 ,  ,  .

Задача 6

править

Докажите, что разложение в цепную дробь эквивалентно алгоритму Евклида. Докажите, что   и число шагов в алгоритме Евклида для пары   равно  . Верно ли, что  ?

Задача 7

править

Докажите, что на парах вида  ,   алгоритм Евклида дольше всего работает при  , и число шагов алгоритма при этом равно  .

При построении рекурсивной функции важно ответить на следующие вопросы:

  1. Что будет базой рекурсии?
  2. Что будет шагом рекурсии?
  3. Почему выполнение шагов рекурсии всегда приведёт к базе?
  4. Какова будет глубина рекурсии при данном значении аргументов? Глубина рекурсии — это глубина дерева рекурсивных вызовов, то есть длина максимального пути по стрелочкам из вершины до одного из элементарных (базовых) значений функции.
  5. Как растёт размер дерева рекурсивных вызовов при увеличении входных данных? Будут ли повторяющиеся вызовы в этом дереве и имеет ли смысл делать рекурсию с запоминанием?

Одно из важных достоинств рекурсивных алгоритмов заключается в том, что они просты и наглядны. Но рекурсия не всегда является эффективным (самым быстрым) решением. Рекурсия использует мало памяти, но работать может довольно долго, как в примере с числами Фибоначчи.

Задача 8

править

Есть несколько алгоритмов Евклида, основанных на различных рекуррентных соотношениях для НОД:

  1.  ;
  2.  
  3.  

Эффективность этих алгоритмов можно оценивать с помощью среднего числа шагов алгоритма, необходимых для вычисления  , где   фиксированно, а   пробегает значения  . Известно, что это число примерно пропорционально  . Вычислите экспериментально коэффициенты пропорциональности для указанных трёх случаев. Явные выражения для этих коэффициентов довольно сложны.

Задачи для самостоятельного решения

править

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

Ниже предлагается несколько простых задач, для знакомства с идеей рекурсии.

Задача 9

править

Напишите две рекурсивные процедуры вычисления  , основанные на двух различных соотношениях:

  1.  
  2.  

База рекурсии в обоих случаях  . Какое получается дерево рекурсивных вызовов при  ? Имеет ли смысл запоминать вычисленные значения? Чему равно число шагов при  ? Какая из процедур оказалась более эффективной? Покажите, что в случае (2) число рекурсивных вызовов (число вершин в дереве рекурсивных вызовов) ограничено сверху числом  .

Задача 10

править

 

Найдите число различных путей из точки   в точку   по стрелочкам. Напишите программу, которая вычисляет число этих путей для таких треугольных конфигураций (графов) со стороной  .

Задача 11

править

Число правильных скобочных структур длины 6 равно 5: ()()(), (())(), ()(()), ((())), (()()).

Напишите рекурсивную программу генерации всех правильных скобочных структур длины  . Определение правильной скобочной структуры можно задать в нотации EBNF (в расширенной форме Бэкуса — Наура) рекурсивно:

s ::= '(' s ')' | s s | ''

Эта строчка содержит рекурсивное определение объекта s: «объект типа s может быть получен из объекта типа s с помощью окружения его открывающейся и закрывающейся круглой скобки, или с помощью приписывания двух объектов типа s друг к другу, либо это просто пустое слово». Вертикальная черта в нотации EBNF означает союз «или». С помощью одинарных кавычек выделяют символы или строки символов, пробелы играют роль разделителей.

Задача 12

править

Чему равно число   правильных скобочных структур длины  ? Найдите рекуррентную формулу для числа  , а именно выразите   через все предыдущие  . Напишите программу, которая вычисляет число   правильных скобочных структур длины  .

Подсказка: найдите перебором первые элементы последовательности  . Рассмотрите соотношения соседних элементов и догадайтесь до явной формулы.

Второй способ. Задача решается методом динамического программирования. Число различных путей из   в   по стрелкам на рисунке из задачи 10 равно числу правильных скобочных структур длины 6 (стрелка вверх соответствует открывающей скобке, а стрелка вниз — закрывающей). Придумайте способ последовательного вычисления числа различных путей из вершины   до различных вершин графа. Сколько памяти использует ваша программа для вычисления   в зависимости от  ?

Задача 13

править

Известный алгоритм быстрой сортировки Хоара также основан на рекурсии. Пусть нам нужно отсортировать кусочек массива   от элемента с индексом   по элемент с индексом   включительно. Перераспределим элементы на этом кусочке так, чтобы вначале стояли элементы меньшие либо равные  , а потом элементы большие либо равные  . Пусть последний элемент в первом кусочке оказался на месте  . Вызовем рекурсивно сортировку двух кусочков от   до   и от   до   включительно, (если, конечно, эти кусочки состоят более, чем из одного элемента). Воплотите эту идею в работающую процедуру сортировки массива. Проведите эксперименты по оценке средней глубины дерева рекурсии и времени работы алгоритма в зависимости от размера массива. Рассмотрите случаи а) случайного массива, б) массива, упорядоченного по возрастанию, и в) массива, упорядоченного по убыванию.

Задача коммивояжёра

править

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

Коммивояжёр (франц. commis voyageur), разъездной представитель торговой фирмы, предлагающий покупателям товары по имеющимся у него образцам, каталогам и тому подобное.

Наш коммивояжёр ездит по городам с целью сбыта товаров разного рода. Он всегда начинает и заканчивает свой путь в одном и том же городе. На проживание во всех городах коммерсант тратит одинаковую сумму денег. Поэтому существенна только сумма на проезд из города в город. Есть список городов и стоимость проезда между некоторыми парами городов.

Задача коммивояжёра — побывать во всех городах (не менее одного раза) и при этом потратить как можно меньше денег на проезд и вернуться обратно.

Формулировка задачи

  • внутри города проезд ничего не стоит;
  • проезд между двумя городами напрямую стоит одинаково в оба конца;
  • стоимость — целое число от 1 до 10000;
  • городов не более 100.

Стоимости проезда между парами городов записаны в следующем формате:

<Количество стоимостей> <Начальный город>
<Город> <Город> <Стоимость>
<Город> <Город> <Стоимость>
<Город> <Город> <Стоимость>
…

Результат записывается в следующем формате:

<Количество городов в пути>
<Город> <Город> <Город> …

Город задаётся названием без пробела. Длина названия города не более 30 символов. Программа должна реагировать на клавишу ESC. Если ESC была нажата, то в течении 10 секунд программа должна записать результат и закончить работу.

Кажется, что для решения такой задачи достаточно хранить самые оптимальные промежуточные пути и вот он динамический алгоритм. Например, рассмотрим тройки городов и для каждой тройки определим в какой последовательности лучше всего её проходить. Затем рассмотрим все четвёрки городов. Решения для четвёрок можно найти на основе известных решений для троек и так далее. Но дело в том, что число возможных наборов   городов и   возможных очень быстро растёт с   и  . Например, 50 городов из 100 можно выбрать 100 891 344 545 564 193 334 812 497 256 способами. Таким образом, запоминать промежуточные решения нет никакой возможности — их слишком много даже для  , а на практике нужно решать задачи с  . Для задачи коммивояжёра на плоскости можно использовать ряд эвристических идей, которые позволяют находить приемлемые решения за разумное время. Но при этом даже для случая точек на плоскости задача коммивояжёра остаётся полиномиально неразрешимой, то есть не существует алгоритма, который бы находил самый оптимальный путь коммивояжёра за время ограниченное полиномом   произвольной степени   при любой константе  .

Снежинка Коха

править

Снежинка Коха — это фрактальное множество точек на плоскости, которое похоже на снежинку.

 

Здесь приведена программа на языке PostScript. Этот код интерпрерируется программой GSView, которую можно взять на сайте http://www.ghostscript.com.

Снежинка рисуется рекурсивным образом. Сначала она выглядит как треугольник. Затем на сторонах этого треугольника рисуются треугольные выступы. Получается шеcтиконечная звезда. На сторонах этой звезды снова рисуются треугольные выступы (см. синюю фигуру). Процесс наращивания треугольных выступов можно продолжить до бесконечности и получить в пределе вполне корректно определённое множество точек на плоскости.

В математике, в отличие от программирования, допускаются такие бесконечные рекурсивные определения. На каждом шаге у нас получается некоторая обычная фигура (не фрактал), а в пределе (после бесконечного числа шагов) получается фрактал. Если положить, что длина стороны исходного треугольника равна 1, то длина стороны шестиугольной звезды равна  . Длина стороны следующей фигуры ещё в три раза меньше, то есть  . Можно записать общую формулу:  . Заметьте, что число сторон   растёт от номера шага как  , то есть  . Периметр   фигуры, получающейся на шаге  , есть произведение числа сторон на длину:  . Таким образом, периметр фигуры растёт с каждым шагом и стремится к бесконечности.

Задача. Пусть две вершины начального правильного треугольника лежат в точках   и  . Какие ещё есть точки с рациональными координатами, принадлежащие снежинке Коха?

Программа «Снежинка Коха»

%!PS-Adobe
 /inch {72 mul} def
 /depth 6 def       % глубина рекурсии
 /baseX 1 inch def  % положение левого нижнего
 /baseY 5 inch def  % угла исходного треугольника
 /edge  6 inch def  % длина стороны треугольника
 0.8 setlinewidth   % толщина линии
 % buildElem - ГЛАВНАЯ РЕКУРСИВНАЯ ФУНКЦИЯ
 /buildElem {
   2 copy
   /recDepth 0 def
   /L 0 def
   /L exch store        % L        = arg2
   /recDepth exch store % recDepth = arg1
   recDepth 0 le {
     newpath 0 0 moveto L 0 rlineto stroke
   } {
     gsave
       /recDepth recDepth 1 sub store
       /delta L 3 div def
       recDepth delta buildElem
       gsave
         dup 0 translate 60 rotate
         buildElem
         dup 0 translate -120 rotate
         buildElem
       grestore
       dup 2 mul 0 translate
       buildElem
       pop pop
     grestore
   } ifelse
 } def
 gsave
   baseX baseY translate 60 rotate
   depth edge buildElem
   edge 0 translate -120 rotate
   buildElem
   edge 0 translate -120 rotate
   buildElem
   pop pop
 grestore
 stroke
 showpage

Заключение

править

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

Дальнейшее чтение

править
  1. Беллман Р. Динамическое программирование. — М.: Изд-во иностр. лит., 1960.
  2. Ахо А., Хопкрофт Дж., Ульман Дж. Построение и анализ вычислительных алгоритмов. — М.: Мир 1979.
  3. Кормен Т., Лейзерсон Ч., Ривест Р. Алгоритмы: построение и анализ. — М.: МЦНМО, 1999.