Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ОЛИМПИАДы ПО ПРОГРАММИРОВАНИЮ.doc
Скачиваний:
20
Добавлен:
25.04.2019
Размер:
256.51 Кб
Скачать

Занятие № 2 Динамическое программирование

Е.В. Брызгалов

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

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

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

Первые две задачи, строго говоря, нельзя отнести к указанному классу, но приемы, использованные при их решении, очень сходны с таковыми у задач, рассматриваемых на этом занятии. Остальные задачи в свое время встречались на различных олимпиадах (а некоторые с тех пор стали "фольклорными") и расположены (по мнению автора публикации) в порядке возрастания сложности. Для простоты будем считать, что в большинстве задачах исходные данные таковы, что результат поместится в тип LongInt. Справедливости ради отметим, что такое ограничение существует не всегда, и в последних двух задачах приходится либо использовать тип Double, либо дополнительно реализовывать "длинную арифметику".

Числа Фибоначчи Вычислить N-ое число в последовательности Фибоначчи, — 1, 1, 2, 3, 5, 8, ... — в которой первые два члена равны единице, а все остальные представляют собой сумму двух предыдущих. Формат входных данных Одно число 0 < N < 47. Формат выходных данных Одно число — N-ый член последовательности.

[посмотреть подсказку]

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

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

Самый очевидный способ “решения” задачи состоит в написании рекурсивной функции примерно следующего вида:

Function F(X:integer):longint;

Begin

if (X = 1) or (X = 2) then F := 1 else F := F(X - 1) + F(X - 2)

end;

При этом где-то в середине четвертого десятка программа “подвешивает” самый быстрый компьютер (напомним, что по правилам олимпиад время на прохождение теста ограничивается, чаще всего это 10–15 секунд). Попробуем разобраться, почему так происходит? Для вычисления F(40) мы сперва вычисляем F(39) и F(38). Причем F(38) мы считаем “по новой”, “забывая”, что уже вычислили его, когда считали F(39). То есть наша основная ошибка в том, что значение функции при одном и том же значении аргумента считается много (слишком много!) раз. Если исключить повторный счет, то функция станет заметно эффективней. Для этого приходится завести массив, в котором хранятся значения нашей функции:

Var D : Array [1..50] of LongInt;

Срабатывает золотой закон программирования — выигрывая в скорости, проигрываем в памяти. Сперва массив заполняется значениями, которые заведомо не могут быть значениями нашей функции (чаще всего, это “минус единица”, но в нашей задачке вполне годится для этих целей “ноль”). При попытке вычислить какое-то значение, программа смотрит, не вычислялось ли оно ранее, и если да, то берет готовый результат. Функция принимает следующий вид (не верьте, пожалуйста, книгам, утверждающим, что искать числа Фибоначчи рекурсивно нельзя в принципе — можно, если отсечение делать с умом):

Function F(X : integer) : LongInt;

Begin

if D[X] = 0

then

if (X = 1) or (X = 2)

then D[X] := 1

else D[X] := F(x - 1) + F(x - 2);

F := D[X]

End;

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

D[1] := 1; D[2] := 1;

For i := 3 to X do D[i] := D[i-1] + D[i-2];

Чаще всего такой способ раза в три быстрее. Но очень часто для его написания приходится использовать как промежуточный результат нисходящую форму, а иногда безрекурсивная (итеративная) форма оказывается чрезвычайно сложной и малопонятной. Не стоит забывать и о том, что время на олимпиаде ограничено. Еще об одном недостатке второй формы будет сказано через несколько задач. Суммируя, можно дать совет участнику (отнеситесь к нему критически): “Пишите и тестируйте рекурсивную форму, а переделыванием занимайтесь, если ваша программа превышает отведенное ей время на “больших” тестах”.

к занятию № 2

Мячик на лесенке На вершине лесенки, содержащей N ступенек, находится мячик, который начинает прыгать по ним вниз, к основанию. Мячик может прыгнуть на следующую ступеньку, на ступеньку через одну или через 2. (То есть, если мячик лежит на 8-ой ступеньке, то он может переместиться на 5-ую, 6-ую или 7-ую.) Определить число всевозможных "маршрутов" мячика с вершины на землю. Формат входных данных Одно число 0 < N < 31. Формат выходных данных Одно число — количество маршрутов.

[посмотреть подсказку]