Первая страница / Теория / Построение и анализ алгоритмов /

Динамическое программирование

Голосование: 28, 1

Введение

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

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

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

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

Задача о наибольшей общей подпоследовательности

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

Пусть у нас есть последовательность X = {x1x2, …, xm}, тогда другая последовательность Z = {z1z2, …, zk} будет подпоследовательностью X, если существует такая возрастающая последовательность индексов I = {i1i2, …, im}, что для всех j = 1, 2, …, k, будет верно равенство xij = zj.

Пример. Z = {B, C, D, B} — подпоследовательность X = {A, B, C, B, D, A, B} с набором индексов {2, 3, 5, 7}.

Говорят, что Z — общая подпоследовательность X и Y, если она является подпоследовательностью для X и Y. Тогда определим наибольшую общую подпоследовательность (НОП) двух данных последовательностей как общую подпоследовательность с максимальной длиной.

Теперь задача окончательно формализована, и можно приступать к ее решению. Введем понятие i-го префикса для заданной входной строки. Пусть имеется последовательность X = {x1x2, …,xm}. Определим ее i-ый префикс (i = 1, 2, …, m) как Xi = {x1x2, …, xi}, а X0 соответственно пустая последовательность. Для того, чтобы лучше понять структуру НОП, весьма полезна следующая теорема.

Теорема (о структуре НОП)

Пусть Z = {z1z2, …, zk} — это любая НОП для последовательностей X = {x1x2, …, xn} и Y = {y1y2, …, ym}. Тогда

  1. Если xn = ym, то zk = xn = ym и Zk−1 — это НОП Xn−1 и Ym−1.
  2. Если xn ≠ ym и zk ≠ xn, то Z — это НОП Xn−1 и Y.
  3. Если xn ≠ ym и zk ≠ ym, то Z — это НОП X и Ym−1.

Доказательство.

  1. Предположим, что zn ≠ xn. Тогда мы можем присоединить xn = ym к Z и получить общую последовательность для X и Y длины k+1, но это противоречит нашему предположению о том, что Z — это НОП X и Y. Таким образом, zk = xn = ym. Теперь, префикс Zk−1 — это общая подпоследовательность для Xn−1 и Yn−1 длины k−1. Покажем, что она является НОП для Xn−1 и Yn−1. Предположим, что это не так, тогда существует общая подпоследовательность W дляXn−1 и Yn−1 длины большей, чем k−1. Тогда присоединяя к W xn = ym получим подпоследовательность общую для X и Y, длины большей, чем k, что противоречит условию.
  2. Нужно показать, что если zn ≠ xn, то Z — это НОП Xn−1 и Y. Предположим, что это не так, тогда существует подпоследовательность W общая подпоследовательность для Xn−1 и Y длины большей k. Но тогда W также общая подпоследовательность для Xn и Y, что противоречит тому, что Z это НОП для X и Y.
  3. Доказательство этого пункта совершенно аналогично предыдущему.

Непосредственно из теоремы следует способ нахождения НОП двух заданных последовательностей X = {x1x2, …, xn} и Y = {y1y2, …, ym}. Если xn = ym, то мы должны искать НОП для Xn−1 и Ym−1 и, присоединив к этому результату xn = ym, мы получим НОП для X и Y. Если же xn ≠ ym, то мы должны решить две подзадачи: первая НОП Xn−1 и Y, вторая найти НОП X и Ym−1. Затем выбрать наибольший из двух результатов, это и будет НОП Xи Y. Заметим, что каждая из двух подзадач потребует решения вновь появившихся подзадач, которые в свою очередь будут выявлять все новые подзадачи.

Остается только выбрать какой из двух алгоритмов прогонки здесь больше подходит, прямой или обратной. С вычислительной точки зрения наиболее рациональным будет алгоритм прямой прогонки, вычисляющий НОП снизу вверх. В случае вычисления очень удобно организовать в виде таблицы c[0…n, 0…m], где c[i][j] соответствует длине НОП префиксов Xi и Yj. Значение c[i][j] можем вычислять по следующим правилам:

  • 0, если i = 0 или j = 0;
  • c[i−1][j−1] + 1, если ij > 0 и xi = yj;
  • max(c[i][j−1], c[i−1][j]), если ij > 0 и xi ≠ yj.

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


LCS-LENGTH(X,Y)
 1 m = length[X]
 2 m = length[Y]
 3 for i = 1 to m
 4   do c[i, 0] = 0
 5 for i = 1 to m
 6   do c[0, j] = 0
 7 for i = 1 to m do
 8   do for j = 1 to n
 9     do if xi = yi
10            then c[i,j] = c[i − 1, j − 1] + 1;
11                 b[i,j] = «стрелка по диагонали»
12            else if c[i − 1, j] ≥ c[i, j - 1]
13                    then c[i, j] = c[i − 1, j]
14                         b[i, j] = «стрелка вверх»
15                    else c[i, j] = c[i, j − 1]
16                         b[i, j] = «стрелка влево»
17 return b, c

На следующем рисунке показана работа алгоритма для X = {A, B, C, B, D, A, B} и Y = {B, D, C, A, B, A}

Результат работы алгоритма на примере

Теперь покажем, как непосредственно найти саму НОП.


PRINT_LCS(b, X, i, j)
1 if i = 0 or j = 0
2    then return
3 if b[i, j] = «стрелка по диагонали»
4    then PRINT_LCS(b, X, i − 1, j − 1)
5         print vi
6 elseif b[im j] = «стрелка вверх»
7    then PRINT_LCS(b, X, i − 1, j)
8 else PRINT_LCS (b, X, i, j − 1)

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

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

Шаблоном называется строка состоящая из букв латинского алфавита (a, …, z, A, …, Z) и символов ? и *. Каждый из символов ? разрешается заменить на одну произвольную букву, а каждый из символов * на произвольную (возможно пустую) последовательность букв. Про любую строку из букв, которую можно получить из шаблонов такими заменами, будем говорить, что она удовлетворяет данному шаблону. Тогда задачу состоит в том, чтобы для двух заданных шаблонов найти строку минимальной длины, которая удовлетворяет обоим шаблонам, либо выяснить, что такой строки не существует.

Нельзя не отметить, что подобного рода задачи, находят большое применение на практике, обычно они возникают при анализе и разборе регулярных выражений. Итак, пусть заданы два шаблона, S[1…M] и T[1…N]. Введем обозначение F(ij) для строки минимальной длины, которая удовлетворяет шаблонам S[1…i] и T[1…j]. Если такой строки нет, то в F(ij), будет стоять специальная пометка, говорящая об этом.

Вычисляем значение F(ij) в порядке возрастания i, а при равных i, в порядке возрастания j. Возможны следующие содержательные ситуации (считаем, что ij > 0, а разбор граничных случаев, не представляет особого интереса, и может быть оставлен как упражнение).

  1. S[i] и T[j] — буквы. Если они совпадают, то в качестве значения F(ij) берем F(i−1, j−1) с добавленной в конце этой буквой. Если в F(i−1, j−1) стоит пометка, говорящая о том, что строки не существует, то аналогичную пометку можно поставить и в F(ij). В случае несовпадения букв S[i] и T[j], в F(ij) необходимая строка также не существует.
  2. S[i] и T[j] — буква и символ «?» или два символа «?». Поступаем точно так же, как и в предыдущей ситуации, однако случае несовпадения букв из-за наличия «?» здесь быть не может.
  3. S[i] — символ «*», а T[j] — буква или символ «?». В данной ситуации выбираем наиболее короткое среди значений F(ij−1) и F(i−1, j) с дописанной к нему буквой T[j] (или любой буквой, если T[j] есть символ «?» ).
  4. S[i] — буква или символ «?», а T[j] — символ «*». Поступаем аналогично разобранному выше пункту.
  5. S[i] и T[j] — два символа «*». В этом случае в качестве F(ij), берем наиболее короткое из значений F(ij−1) и F(i−1, j).

Задача триангуляции

В качестве еще одного примера динамического программирования рассмотрим задачу триангуляции многоугольника. Заданы вершины многоугольника и расстояния между каждой парой вершин. Это расстояние может быть геометрическим расстоянием на плоскости или произвольной функцией стоимости, заданной в виде таблицы. Задача заключается в том, чтобы выбрать такую совокупность хорд (линий между несмежными вершинами), что никакие две хорды не будут пересекаться, а весь многоугольник будет поделен на треугольники. Общая длин всех хорд должна быть минимальной. Такая совокупность хорд называется минимальной триангуляцией. Зафиксируем несколько фактов, которые помогут разработать необходимый алгоритм. Предполагаем, что наш многоугольник имеет n вершин v0v1, …, vn−1, перечисленных по часовой стрелке.

  1. В случае триангуляции любого многоугольника, содержащего более трех вершин, с каждой парой смежных вершин связана, по крайней мере одна хорда. Чтобы убедиться в этом, предположим, что ни vi, ни vi+1 не связаны ни с одной из хорд. В таком случае область, ограничиваемая стороной (vivi+1), должна бы включать стороны (vi−1vi), (vi+1vi+2) и по крайней мере еще одну дополнительную сторону или хорду. Но в таком случае это область не была бы треугольником.
  2. Если (vivj) является хордой в триангуляции, значит должна найтись, такая вершина vk что каждая из линий (vivk) и (vkvi) либо стороной многоугольника либо хордой . В противном случае хорда (vivj), ограничивала бы область, не являющуюся треугольником.

Чтобы приступить к поиску минимальной триангуляции, выберем две смежные вершины, например v0 и v1. Из указанных выше фактов следует, что в любой триангуляции (а значит и в минимальной) должна найтись такая вершина vk, что (v1vk) и (vkv0) являются хордами или сторонами многоугольника. Необходимо рассмотреть, насколько приемлемую триангуляцию можно построить после выбора каждого значения k. Если в многоугольнике n вершин, то в нашем распоряжении имеется (n−2) возможных вариантов. Каждый вариант выбора k приводит к не более чем двум подзадачам, где мы имеем многоугольники образованные, одной хордой и сторонами исходного многоугольника. Например, могут возникнуть такие подзадачи при случае выбора вершины v3.

Рис. 1. Две подзадачи, возникающие после выбора вершины v3

Далее нужно найти минимальную триангуляцию для многоугольников, полученных в результате появления новых подзадач. Правда, если мы будем решать все подзадачи, которые будут появляться, то мы получим алгоритм с экспоненциальной трудоемкостью. Но, имея треугольник, включающий хорду (v0, vk), нам никогда не придется рассматривать многоугольники, у которых более одной стороны являются хордами исходного многоугольника. «Факт 2» говорит о том, что при минимальной триангуляции хорда в подзадаче (например, хорда (v0v3) на рис. 1) должна составлять треугольник с одной из остальных вершин многоугольника.

Определим подзадачу размера s, начинающуюся с вершины vi и обозначаемую Sis, как задачу минимальной триангуляции для многоугольника, образованного s вершинами, начинающегося с вершины vi и содержащего вершины vivi+1, …, vi+s−1, перечисляемые в порядке обхода вершин по часовой стрелке. В v0 хордой является замыкающая сторона (vivi+s−1). Чтобы решить подзадачу Sis необходимо рассмотреть три варианта:

  1. Можно выбрать вершину vi+s−2, чтобы составить треугольник с хордами (vivi+s−1), (vivi+s−2) и третьей стороной (vi+s−2vi+s−1), а затем решить подзадачу Si,s−1.
  2. Мы можем выбрать вершину vi+1, чтобы составить треугольник с хордами (vivi+s−1), (vi+1vi+s−1) и третьей стороной (vivi+1), а затем решить подзадачу Si+1,s−1.
  3. Для некоторого k из диапазона от 2 до s−3 можно выбрать вершину vi+k и образовать треугольник со сторонами (vivi+k), (vi+kvi+s−1), (vivi+s−1), а затем решить подзадачи Si,k+1 и Si+k,sk.

Если учесть, что решение подзадачи размером не более трех не требует вообще никаких действий, то рассматривая описанные варианты 1–3, следует предпочесть вариант 3, где нужно выбрать некоторое k из диапазона от 1 до s−2 и решить подзадачи Si,k+1 Si+k,sk. Если для решения задачи размером более четыре и более воспользоваться очевидным рекурсивным алгоритмом, непосредственно вытекающим из перечисленных выше вариантов, то можно показать что общее число рекурсивных вызовов составляет 3s−4, если подсчитать лишь обращения к подзадачам размером четыре и более. Таким образом количество подзадач, которые необходимо решить, возрастает по экспоненциальному закону в зависимости от s. А следовательно общее количество шагов экспоненциально зависит и от n.

Рис. 2. Разбиение задачи Sis на подзадачи

Но известно, что, помимо исходной задачи существует лишь n(n−4) различных подзадач, которые в любом случае необходимо решить. Значит в приведенном анализе что-то явно не совсем верно. Очевидно, что совсем не все подзадачи отличаются между собой, если действовать рекурсивно, как приведено выше, то у нас возникают ситуации, когда одни и те же ситуации приходится решать несколько раз. Именно это подсказывает нам эффективный способ вычисления триангуляции. Прежде всего, вычисления удобно организовать в виде таблицы. Назначим стоимость Cis триангуляции Sis для всех i и s. Поскольку решение любой данной задачи зависит от решения задач меньшего размера, то логичным порядком заполнения такой таблицы является порядок, основанный на размерах подзадач, т. е. для размеров s = 4, 5, …, n−1 мы указываем минимальную стоимость задач Sis для всех вершин i. Удобно включить и задачи размеров 0 ≤ s < 4, но при этом нужно помнить, что Sis имеет стоимость 0, если s < 4.

В соответствии с перечисленными выше вариантами действий 1–3 при определении подзадач формула вычисления Cis при s ≥ 4 должна иметь следующий вид:

Cis = min1≤ks−2 {Ci,k+1 + Ci+k,sk + D(vivi+k) + D(vi+kvi+s−1)},

где D(vpvq) — это длина хорды между вершинами vp и vq, если vp и vq не являются смежными вершинами многоугольника; D(vpvq) равняется 0, если являются смежными вершинами.

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

Задача о загрузке

Задача о загрузке — это задача о рациональной загрузке судна (самолета, автомашины, и т. п.), которое имеет ограничения по объему или грузоподъемности. Каждый помещенный на судно груз приносит определенную прибыль. Задача состоит в том, чтобы определении загрузки, такими грузами которые приносят наибольшую прибыль.

Отметим также, что данная задача известна как задача о рюкзаке, в который турист должен определить наиболее ценные предметы, подлежащие загрузке в рюкзак. Формализуем нашу задачу. Пусть W — грузоподъемность нашего судна и есть в наличии n наименований предметов, которые подлежат загрузке. Пусть mi — количество предметов i-го наименования, ri — прибыль, которую приносит один загруженный предмет i-го наименования, wi — вес одного предмета i-го наименования. Тогда общая задача имеет вид следующей целочисленной задачи линейного программирования.

Максимизировать Z = r1m1 + r2m2 + … + rnmn при условии, что

w1m1 + w2m2 + … + wnmn ≤ W,
m1m2, …, mn ≥ 0.

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

  1. Определение этапов.
  2. Определения на каждом этапе вариантов решения (альтернатив).
  3. Определения состояний на каждом этапе.

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

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

В приведенной задаче о загрузке три элемента модели динамического программирования определяются следующим образом.

  1. Этап i ставится в соответствие предмету i-го наименования, i = 1, 2, …, n.
  2. Варианты решения на этапе i описываются количеством предметов mi предметов i-го наименования, подлежащих загрузке. Соответствующая прибыль равна rimi. Значение заключено mi в пределах от 0 до [Wwi], где [Wwi] — целая часть числа Wwi.
  3. Состояние xi на i выражает суммарный вес предметов, решения о погрузке которых приняты на этапах ii+1, …, n. Это определение отражает тот факт, что ограничение по весу является единственным, которое связывает вместе все n этапов.

Пусть fi(xi) — максимальная суммарная прибыль от этапов ii+1, …, n при заданном состоянии xi. Определим рекуррентное уравнение с помощью следующей двухшаговой процедуры.

  1. Выразим fi(xi) как функцию fi+1(xi+1) в виде
    fi(xi) = max {rimi + fi+1(xi+1)}, i = 1, 2, …, n,
    где max берется по всем
    mi = 0, 1, …, [Wwi] и xi = 0, 1, …, W,
    где fn+1(xn+1) ≡ 0.
  2. Выразим xi+1 как функцию xi для гарантии того, что левая часть последнего уравнения является функцией лишь xi. По определению xi+1 − xi представляет собой вес, загруженный на этапе i, т. е. xi+1 − xi = wimi или xi+1 = xi − wimi. Тогда рекуррентное уравнение приобретает следующий вид.
    fi(xi) = max {rimi + fi+1(xiwimi)}, i = 1, 2, …, n,
    где max берется по всем
    mi = 0, 1, …, [Wwi] и xi = 0, 1, …, W.

Задача о загрузке является типичным представителем задачи распределения ресурсов, в которой ограниченный ресурс распределяется между конечным числом видов (экономической) деятельности. В таких моделях определения состояния на каждом этапе будет аналогично приведенному для задачи о загрузке : состоянием на этапе i является суммарное количество ресурса, распределяемого на этапах ii+1, …, n.

Заключение

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

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

Литература

  1. Кормен Т., Лейзерсон Ч., Ривест Р. Алгоритмы: построение и анализ. — М.: МЦНМО, 1999.
  2. Ахо А., Хопкрофт Д., Ульман Д. Структуры данных и алгоритмы. — М.: Издательский дом «Вильямс», 2001.
  3. Таха Х. Введение в исследование операций. — М.: Издательский дом «Вильямс», 2001.
  4. Беров В.И., Лапунов А.В., Матюхин В.А., Пономарев А.Е. Особенности национальных задач по информатике. — Киров: Триада-С, 2000.

Камышан Андрей


Маша / 2009-05-27 21:27:59

Хочу сказать спасибо автору за такую статью. Кучу сайтов, посвященных информатике, пересмотрела, но нигде такой доступной раскладки алгоритма LCS не встретила. Да и к тому же везде статьи повторяются. Еще раз огромное спасибо!!!!

Виталий / 2010-07-08 12:21:22

Похоже, что в задаче о загрузке есть ошибка:

"... По определению x(i+1) &#8722; x(i) представляет собой вес, загруженный на этапе i, т. е. x(i+1) &#8722; x(i) = w(i)m(i) или x(i+1) = x(i) &#8722; w(i)m(i). Тогда рекуррентное уравнение приобретает следующий вид.

fi(x(i)) = max {r(i)m(i) + f(i+1)(x(i)&#8722;w(i)m(i))}, i = 1, 2, ?, n,..."

Если x(i+1) &#8722; x(i) = w(i)m(i), то логично, чтобы x(i+1) = x(i) + w(i)m(i), а не x(i+1) = x(i) &#8722; w(i)m(i).

Миша / 2011-11-14 00:35:37

Автору спасибо за статью. Маша, ты не читала Окулова? Это очень толковая книга и там про lcs хорошо написано

Ваше имя
Email
Текущий день недели (строчными буквами)
Комментарий