Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
__Динамическое программирование_специалитет.doc
Скачиваний:
3
Добавлен:
01.05.2025
Размер:
2.74 Mб
Скачать

План решения задачи методом динамического программирования.

  1. Записать то, что требуется найти в задаче, как функцию от некоторого набора аргументов (числовых, строковых или еще каких-либо).

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

  3. Задать начальные значения функции, то есть те наборы аргументов, при которых задача тривиальна и можно явно указать значение функции.

  4. Создать массив (или другую структуру данных) для хранения значений функции. Как правило, если функция зависит от одного целочисленного параметра, то используется одномерный массив, для функции от двух целочисленных параметров – двумерный массив и т. д.

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

Примеры задач, решаемых при помощи динамического программирования. Числа Фибоначчи

Определение. Последовательность Фибоначчи определяется следующим образом:

F0 = 0, F1 = 1, Fn = Fn-1 + Fn-2

Эти числа ввёл в 1202 г. Леонардо Фибоначчи (Leonardo Fibonacci). Сам Фибоначчи упоминал эти числа в связи с такой задачей: "Человек посадил пару кроликов в загон, окруженный со всех сторон стеной. Сколько пар кроликов за год может произвести на свет эта пара, если известно, что каждый месяц, начиная со второго, каждая пара кроликов производит на свет одну пару?". Решением этой задачи и будут числа последовательности, называемой теперь в его честь. Впрочем, описанная Фибоначчи ситуация – больше игра разума, чем реальная природа. Индийские математики Гопала и Хемачандра упоминали числа этой последовательности в связи с количеством ритмических рисунков, образующихся в результате чередования долгих и кратких слогов в стихах или сильных и слабых долей в музыке. Число таких рисунков, имеющих в целом n долей, равно Fn.

Интересен пример растения – тысячелистника, у которого число стеблей (а значит и цветков) всегда есть число Фибоначчи. Причина этого проста: будучи изначально с единственным стеблем, этот стебель затем делится на два, затем от главного стебля ответвляется ещё один, затем первые два стебля снова разветвляются, затем все стебли, кроме двух последних, разветвляются, и так далее, что и даёт в результате числа Фибоначчи. У многих цветов число лепестков является тем или иным числом Фибоначчи. Также в ботанике известно явление ''филлотаксиса''. В качестве примера можно привести расположение семечек подсолнуха: если посмотреть сверху на их расположение, то можно увидеть одновременно две серии спиралей (как бы наложенных друг на друга): одни закручены по часовой стрелке, другие – против. Оказывается, что число этих спиралей примерно совпадает с двумя последовательными числами Фибоначчи: 34 и 55 или 89 и 144. Аналогичные факты верны и для некоторых других цветов, а также для сосновых шишек, брокколи, ананасов, и т.д. Для многих растений (по некоторым данным, для 90% из них) верен и такой интересный факт. Рассмотрим какой-нибудь лист, и будем спускаться от него вниз до тех пор, пока не достигнем листа, расположенного на стебле точно так же (т.е. направленного точно в ту же сторону). Попутно будем считать все листья, попадавшиеся нам (т.е. расположенные по высоте между стартовым листом и конечным), но расположенными по-другому. Нумеруя их, мы будем постепенно совершать витки вокруг стебля (поскольку листья расположены на стебле по спирали). В зависимости от того, совершать витки по часовой стрелке или против, будет получаться разное число витков. Но оказывается, что число витков, совершённых нами по часовой стрелке, число витков, совершённых против часовой стрелки, и число встреченных листьев образуют 3 последовательных числа Фибоначчи. Числа Фибоначчи обладают множеством интересных математических свойств, некоторые из них:

  • Соотношение Кассини: Fn+1Fn-1 – Fn2 = (-1)n

  • Правило "сложения": Fn+k = FkFn+1 + Fk-1Fn

  • Из предыдущего равенства при k = n вытекает: F2n = Fn(Fn+1 + Fn-1)

  • Из предыдущего равенства по индукции можно получить, что Fnk всегда кратно Fn.

  • Верно и обратное к предыдущему утверждение: если Fm кратно Fn, то m кратно n.

  • НОД-равенство: gcd(Fm,Fn) = Fgcd(m,n)

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

Теорема Цекендорфа утверждает, что любое натуральное число n можно представить единственным образом в виде суммы чисел Фибоначчи:

N = Fk1 + Fk2 + … + Fkr, где k1  k2 + 2, k2  k3 + 2, ….

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

, следовательно

Нетрудно доказать матричное равенство:

Но тогда, обозначая

получаем: .

Таким образом, для нахождения n-го числа Фибоначчи надо возвести матрицу P в степень n. Так как возведение матрицы в n-ую степень можно осуществить за O(logn), получается, что n-ое число Фибоначчи можно легко вычислить за O(logn).

Рассмотрим последовательность Фибоначчи Fi по некоторому модулю p. Тогда она является периодичной, причём период начинается с F1 = 1. Рассмотрим p2 + 1 пар чисел Фибоначчи, взятых по модулю p: . Поскольку по модулю p может быть только p2 различных пар, то среди этой последовательности найдётся как минимум две одинаковые пары. Это уже означает, что последовательность периодична. Выберем теперь среди всех таких одинаковых пар две одинаковые пары с наименьшими номерами. Пусть это пары с некоторыми номерами (Fa, Fa+1) и (Fb, Fb+1). Докажем, что a = 1. Действительно, в противном случае для них найдутся предыдущие пары (Fa-1, Fa) и (Fb-1, Fb), которые, по свойству чисел Фибоначчи, также будут равны друг другу. Однако это противоречит тому, что мы выбрали совпадающие пары с наименьшими номерами, что и требовалось доказать.

Задача. Вычислить N-ое число в последовательности Фибоначчи: 1, 1, 2, 3, 5, 8, ...

Формат входных данных Одно число 0 < N < 47.

Формат выходных данных Одно число – N-ый член последовательности.

Решение

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

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

Примеры задач, решаемых при помощи динамического программирования. Черепашка

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

Формат входных данных Первая строка – N – размер доски. Далее следует N строк, каждая из которых содержит N целых чисел, представляющих доску.

Формат выходных данных Одно число – максимальная сумма.

Решение

Эта задача является классикой динамического программирования. Рассмотреть все возможные маршруты и просчитать их невозможно. Но можно свести задачу к аналогичной. Пусть нам известен “максимальный” путь для всех клеток, кроме правой нижней (функция F(X, Y)). Все нужные маршруты проходят через одну из клеток, смежных с этим углом (их всего две). Максимальный же маршрут проходит через ту клетку из двух, для которой значение функции F больше. Остается только правильно выполнить отсечение.

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

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

Примеры задач, решаемых при помощи динамического программирования. Подпалиндром

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

Формат входных данных Строка длиной не более 100 символов, состоящая из заглавных букв латинского алфавита.

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

Решение

Будем искать палиндромы для подстроки с i-го символа по j-ый включительно, то есть писать функцию F(I, J). Хотелось бы, что она возвращала сам палиндром, но это невозможно из-за нехватки памяти. Ограничимся одной длиной. Размещаем граничные случаи: F(I, I) = 1 и F(I, J) = 0, если I > J. Теперь в рассматриваемой подстроке отделяем первый символ. Есть две возможности (какая из них реализуется, пока не знаем). Отделенный символ не участвует в образовании максимального подпалиндрома – тогда F(I, J) = F(I + 1, J). Если же участвует, то ищем в подстроке с конца символ, совпадающий с отделенным (пусть его позиция K), тогда F(I, J) = 2 + F(I + 1, K – 1). Надо предусмотреть еще случай I = K, а затем отсекать рекурсию известным способом.