- •Самитов р.К.
- •Рекурсивное программирование
- •Задача: фишку можно продвинуть за один шаг на одну или на две клетки вперед (по линии), сколько существует способов продвижения фишки на n клеток вперед?
- •Базовые функции манипулирования списками таковы, что список хорошо просматривать от начала к концу, а хорошо строить – от конца к началу.
Задача: фишку можно продвинуть за один шаг на одну или на две клетки вперед (по линии), сколько существует способов продвижения фишки на n клеток вперед?
Fib(1)=1, Fib(2)=2,
Fib(n)= Fib(n-2){если последний шаг был на две клетки}
+ Fib(n-1){если он был на одну клетку} для n>2.
Отметим, что способы продвижения (маршруты) считаются разными, если они различаются хотя бы в одном шаге (даже если их начальные отрезки одинаковые).
FUNCTION Fib(n:INTEGER):INTEGER; BEGIN
IF n<3 THEN Fib:=n ELSE Fib:=Fib(n-2)+Fib(n-1) END;
Ясно, что этот алгоритм не может быть хорошим. Он многократно теряет время на повторные перевычисления чисел Фибоначчи (в соответствии в деревом подзадач).
Если немного обобщить средства определения функций:
<Fib(1),Fib(2)>= <1,2>,
<Fib(n-1),Fib(n)>= <Fib(n-1),Fib(n-2)+Fib(n-1)> для n>2,
то получим почти то же самое, что известно как индуктивный алгоритм вычисления чисел Фибоначчи. На ObjectPascal такую функцию можно описать так:
TYPE TPair= RECORD p,c:INTEGER END;
FUNCTION FibPair(m:INTEGER):TPair; VAR r:TPair;
BEGIN IF m<3 THEN BEGIN FibPair.p:=1; FibPair.c:=2 END
ELSE BEGIN r:=FibPair(m-1);
FibPair.p:=r.c; FibPair.c:=r.p+r.c END END
Дело в том, что начальное определение неявно, но достаточно ясно подсказывало, какие дополнительные переменные надо ввести в употребление, чтобы описать итеративный алгоритм. Осталось только воспользоваться приемом накапливающих параметров, чтобы задействовать возможность использования этих дополнительных переменных.
FUNCTION Fib2(m,FibP,FibC:INTEGER):INTEGER;
BEGIN IF m<2 THEN Fib2:=FibP ELSE
IF m=2 THEN Fib2:=FibC
ELSE Fib2:=Fib2(m-1,FibC,FibP+FibC) END;
Все три приведенные реализации вычисления чисел Фибоначчи заметно по-разному организуют процесс вычисления (*):
в первой – многократно выполняются повторные вычисления, во второй и третьей – нет повторных вычислений;
в первой и второй – основные вычисления выполняются на этапе реализации плана вычислений (на подъеме), в третьей – в процессе построения плана вычислений (на спуске).
Задача «Обратить список» (без обращения его элементов – тоже возможно списков).
Такую задачу мы уже решали, но на файлах. Схема тогда использованного алгоритма: «Взять первый»; «Обратить остаток»; «Добавить первый в конец».
FUNCTION Обратить(L:TSpisok):TSpisok;
BEGIN IF EQ(L,NIL) THEN Обратить:=NIL ELSE
Обратить:=Добавить(Обратить(CDR(L)),CAR(L))END;
FUNCTION Добавить(L:TSpisok;
y:(TSpisokTAtom)):TSpisok;
BEGIN IF EQ(L,NIL) THEN Добавить:=CONS(y,NIL) ELSE
Добавить:=CONS(CAR(L),Добавить(CDR(L),y)) END;
Оценим, сложность этого алгоритм. Видимо приемлемо точная и естественная оценка времени работы алгоритма соответствует размеру дерева подзадач (как функции от n – длины входного списка). В итоге получим оценку ~n*n.
Алгоритм решения этой задачи с такой оценкой времени видимо не является достаточно хорошим.
«Непроизводительные» затраты времени этого алгоритма связаны с функцией «Добавить».
Необходимость использовать функцию «Добавить» возникла в связи с тем, что базовая функция CONS позволяет добавить элемент в начало списка, а нам потребовалось добавление в конец. Доступ к компонентам списка – стековый. А доступ к компонентам файла как у очереди: просмотр – начиная с одного конца, а добавление – с другого.
Добавление элемента в конец списка фактически реализовано полным перестроением списка.
Интуитивно ясно, что сконструировать обращенный список можно, используя ~n операций CONS. Такой алгоритм можно получить, используя прием с накапливающими параметрами.