Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатика
ИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханика
ОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторика
СоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансы
ХимияЧерчениеЭкологияЭкономикаЭлектроника

Перекрывающиеся подзадачи

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

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

Вернёмся для примера к задаче о перемножении последовательности матриц. Из рисунка 1 видно, что решение каждой из подзадач, записанное в данаой клеточке таблицы, многократно используется процедурой matrix-Сhain-order при решении подзадач из расположенных выше клеточек.

Рис. 2. Дерево рекурсии для REKURSlVE-MATRIX-CHAIN(p, 1,4). В каждой вершине написаны значения i и j. Заштрихованы «лишние» вершины (вычисления в которых повторяют уже проделанные).

Например, m[3, 4] используется четырежды: при вычислении m[2,4], m[1,4], m[3, 5] и m[3,6]. Было бы крайне неэффективно вычислять m[3,4] всякий раз заново. В самом деле, рассмотрим следующий (неэффективный) рекурсивный алгоритм, основанный непосредственно на соотношениях (2) и вычисляющий m[i,j] - минимальное количество умножений, необходимое для вычисления Ai..j = AiAi+1...Aj:

double Recursive_Matrix_Chain(float p[], int i,j);

{ if (i=j)

{ return 0};

m[i][j]=1/0;

for (k+i;k<=j-1;k++)

{q=Recursive_Matrix_Chain(p,i,k)+Recursive_Matrix_Chain(p,k+1,j)

+p[i-1]*p[k]*p[j];

if (q<m[i][j])

{m[i][j]=q}}

return m[i][j];}

На рис. 2 изображено дерево рекурсии для recursive-matrix-chain(p, 1,4). В каждой вершине записаны значения i и j. Обратите внимание, что некоторые пары (i,j) встречаются многократно.

Легко видеть, что время работы recursive-matrix-chain (р, 1, n) зависит от п по меньшей мере экспоненциально. В самом деле, обозначим его T(n) и примем, что время исполнения строк 1-2, а также 6-7, равно единице. Тогда:

В сумме по k каждое Т(i) (при i = 1, 2,...,n -1) встречается дважды, и ещё есть n-1 единиц. Стало быть:

 
 


(4)

 

Докажем по индукции, что Т(п)≥ 2n-l для всех n≥1. При n=1 неравенство выполнено, так как T(1) ≥1=2°. Шаг индукции:

Мы видим, что алгоритм recursive-matrix-chain требует экспоненциального времени. Причина в том, что этот алгоритм многократно встречает одинаковые подзадачи и каждый раз решает их заново. Различных подзадач всего лишь 0(n2), и масса времени теряется на лишнюю работу. Метод динамического программирования позволяет этой лишней работы избежать.

Динамическое программирование «сверху вниз»

Алгоритм раздела 16.1 действовал «снизу вверх». Но тот же приём (исключение повторного решения подзадач) можно реализовать и для алгоритмов работающих «сверху вниз». Для этого нужно запоминать ответы к уже решённым подзадачам в специальной таблице. Сначала вся таблица пуста (т.е. заполнена специальными записями, указывающими на то, что соответствующее значение еще не вычислено). Когда в процессе выполнения алгоритма подзадача встречается в первый раз, её решение заносится в таблицу. В дальнейшем решение этой подзадачи берётся прямо из таблицы. (В нашем примере таблицу ответов завести легко, так как подзадачи нумеруются парами (i,j). В сложных случаях можно использовать хеширование.) По-английски этот приём улучшения рекурсивных алгоритмов называется memoization.

Применим это усовершенствование к алгоритму recursive-matrix-chain

float p[z];

double Memoized_Matrix_Chain(float p[]);

{n=z-1;

for (i=1;i<=n;i++)

{ for (j=i;i<=n;j++)

{m[i][j]=1/0;}}

return Lookup_Chain(p,1,n);

}

 

 

double Lookup_Chain(float p[], int i,j);

{ if (m[i][j]<1/0)

{ return m[i][j]};

if (i=j)

{m[i][j]=0};

else { for (k=i;k<=j-1;k++)

{q=Lookup_Chain(p,i,k)+Lookup_Chain(p,k+1,j)+p[i-1]p[k]p[j];

if (q<m[i][j])

{m[i][j]=q}

return m[i][j];

}

Процедура memoized-matrix-chain, подобно matrix-chain-order, заполняет таблицу т[1.. n, 1.. n], где m[i,j]- минимальное количество умножений, необходимое для вычисления Ai..j. Первоначально m[i,j] = в знак того, что соответствующее место в таблице не заполнено. Если при исполнении lookup-chain (p,i,,j) оказывается, что m[i,j] < , то процедура сразу выдает это значение m[i,j]. В противном случае m[i,j] вычисляется как в процедуре recursive-matrix-chain, записывается в таблицу и выдаётся в качестве ответа. Тем самым вызов lookup-chain (p,i,j) всегда возвращает m[i,j], но вычисления проводятся только при первом таком вызове.

Рис. 2 иллюстрирует экономию, достигаемую заменой recursive-MATRIX-CHAIN на memoized-matrix-chain. Заштрихованные вершины дерева рекурсии соответствуют тем случаям, когда значение m[i,j] не вычисляется, а берётся прямо из таблицы.

Алгоритм memoized-matrix-chain требует времени 0(n3), как и алгоритм matrix-chain-order. В самом деле, каждая из 0(n2) позиций таблицы один раз инициализируется (строка 4 процедуры memoized-matrix-chain) и один-единственный раз заполняется— при первом вызове lookup-chain (p,i,j) для данных i и j. Все вызовы lookup-chain (p,i,j) делятся на первые и повторные. Каждый из 0(n2) первых вызовов требует времени 0(п) (не включая времени работы рекурсивных вызовов lookup-chain для меньших участков); общее время работы есть 0(n3). Каждый из повторных вызовов требует времени 0(1); их число есть 0(n3) (вычисления для каждой из 0(n2) клеток таблицы порождают 0(п) вызовов). Тем самым рекурсивный алгоритм, требующий времени (2n), превратился в полиномиальный, требующий времени 0(n3).

Подведём итоги: задача об оптимальном порядке умножения п матриц может быть решена за время 0(n3) либо «сверху вниз» (рекурсивный алгоритм с запоминанием ответов), либо «снизу вверх» (динамическое программирование). Оба алгоритма основаны на перекрытии подзадач; число подзадач есть 0(n2) и оба алгоритма решают каждую из подзадач лишь единожды.

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


Дата добавления: 2015-12-08; просмотров: 123 | Нарушение авторских прав



mybiblioteka.su - 2015-2024 год. (0.012 сек.)