void Sorting(int array[], int *in_min, int *in_max, int *sorted, unsigned size);
void Merge(int *out1, int *out2, unsigned size, int *sorted);
void main()
{
int *in_max, *in_min, *ptr_sorted;
unsigned index;
// В этом месте должна стоять функция ввода массива array
// Задание границ массива
in_max=&array[NUM-1]; in_min=&array[0];
// Процедура сортировки
ptr_sorted=sorted;
} // Конец main
void Sorting(int array[], int *in_min, int *in_max, int *sorted, unsigned size)
// Функция сортировки массива с помощью техники рекурсии и слияния
{
unsigned size1;
int *max1, *min1, *max2, *min2, *out1, *out2;
if (size==1) *sorted=*in_min;
else
{
size1=size>>1; // Задание размера новых подмассивов
// Динамическое отведение памяти для новых подмассивов out1 и out2, которые
// в дальнейшем будут слиты в один выходной массив sorted
out1=(int far *)farcalloc(size1,sizeof(int));
if(out1==NULL) Mem_error // Проверка на наличие оперативной памяти
out2=(int far *)farcalloc(size1,sizeof(int));
if(out2==NULL) Mem_error // Проверка на наличие оперативной памяти
// Задание границ новых подмассивов
min1=in_min; min2=min1+size1;
max1=min2-1; max2=in_max;
// Сортировка новых подмассивов
// Слияние подмассивов
farfree(out1); farfree(out2); // Освобождение динамической памяти
}
} // Конец Sorting
void Merge(int *out1, int *out2, unsigned size, int *sorted)
// Слияние двух отсортированных в неубывающем порядке подмассивов в один
{
unsigned ind1=0, ind2=0;
while ((ind1<size)&&(ind2<size))
{
if (out1[ind1] < out2[ind2]) *sorted++=out1[ind1++];
else *sorted++=out2[ind2++];
if (ind1==size)
while (ind2<size) *sorted++=out2[ind2++];
if (ind2==size)
while (ind1<size) *sorted++=out1[ind1++];
}
} // Конец Merge
Работа функции Merge состоит в выборе наименьшего из наименьших элементов сливаемых массивов и занесении его в выходной массив. После этого выбранный элемент удаляется из рассматриваемых массивов, и происходит сравнение следующих двух наименьших чисел. Поскольку сливаемые множества S и T сами уже упорядочены, то подобная процедура не требует числа сравнений, большего, чем сумма длин S и T без единицы.
Подсчёт числа сравнений в алгоритме 6 приводит к рекуррентным уравнениям
, (1.10)
решением которых по теореме 1 является C(n)=O(n×logn). Для больших n сбалансированность размеров подзадач дала значительную выгоду.
Ещё один важный момент данной программы заключается в том, что функция Sorting использует динамическую память для сливаемых подмассивов, которая выделяется и освобождается по мере надобности. Данный приём используется очень часто для представления структур, связанных со списками и деревьями.
1.9 Динамическое программирование
Рекурсивная техника полезна, если задачу можно разбить на подзадачи за разумное время, а суммарный размер подзадач будет небольшим. Из теоремы 1 следует, что если сумма размеров подзадач равна a×n для некоторой постоянной a>1, то рекурсивный алгоритм, вероятно, имеет полиномиальную временную сложность. Однако если очевидное разбиение задачи размера n сводит её к n подзадачам размера n-1, то рекурсивный алгоритм, вероятно, имеет экспоненциальную сложность. В этом случае часто можно получить более эффективные алгоритмы с помощью техники, называемой динамическим программированием (dynamic programming).
Динамическое программирование вычисляет решения для всех подзадач. Вычисление идёт от малых подзадач к бóльшим, и ответы запоминаются в таблице. Преимущество этого метода состоит в том, что раз уж подзадача решена, её ответ где-то хранится и никогда не вычисляется заново. Попробуем рассмотреть данную технику на простом примере.
Пусть требуется вычислить произведение n матриц
M = M1´M2´…´Mn,
где Mi - матрица с числом строк ri-1 и числом столбцов ri (размера ri-1´ri). Порядок, в котором перемножаются данные матрицы, может существенно сказаться на общем числе операций, требуемых для вычисления M, независимо от алгоритма, используемого для умножения матриц.
Пример 1.7. Предположим, что для умножения матрицы размера (p´q) на матрицу (q´r) требуется p´q´r операций, как это бывает в «обычном» алгоритме, и рассмотрим произведение:
M = M1 ´ M2 ´ M3,
(10´20) (20´1) (1´50)
где внизу указаны размеры матриц. Если теперь вычислить M в порядке M1´(M2´M3), то потребуется 11000 операций. Когда же произведение M будет вычислено в порядке (M1´M2)´ M3, то потребуется всего 700 операций.ð
Доказано, что процесс перебора всех возможных порядков, в которых можно вычислить рассматриваемое произведение n матриц, с целью минимизировать число операций имеет экспоненциальную сложность O(2n), что для больших n практически неприемлемо. Однако динамическое программирование приводит к алгоритму сложности O(n3). Пусть mij - минимальная сложность вычисления Mi´Mi+1´…´Mj (1£ i£ j£ n). Очевидно, что
(1.11)
Здесь mik - минимальная сложность вычисления M′=Mi+1´Mi+2´…´Mk, а mk+1,j - минимальная сложность вычисления M″=Mk+1´Mk+2´…´Mj. Третье слагаемое равно сложности умножения M′ на M″. Поскольку матрица M′ имеет размер ri-1´ rk, а матрица M″ - размер rk´ rj. В (1.11) утверждается, что mij (j > i) - наименьшая из сумм этих трёх членов по всем возможным значениям k, лежащим между i и j-1.
При динамическом программировании mij вычисляются в порядке возрастания разностей нижних индексов. Начинают с вычисления mii для всех i, затем mi,i+1 для всех i, потом mi,i+2 и т.д. При этом mik и mk+1,j в (1.11) будут уже вычислены, до того, как приступают к вычислению mij. Это следует из того, что при i £ k < j разность j-i должна быть больше k-i, а также и j-(k+1).
Алгоритм 7. Динамического программирования для вычисления порядка, минимизирующего сложность умножения цепочки из n матриц M1´M2´…´Mn
Вход. r0, r1, …, rn, где ri-1 и ri - числа строк и столбцов матрицы Mi.
Выход. Минимальная сложность умножения матриц Mi в предположении, что для умножения (p´q)-матрицы на (q´r)-матрицу требуется p×q×r операций.
Метод. Алгоритм заполняет последовательно «верхнетреугольную» матрицу минимальных сложностей умножения двух, трех, четырех, … и т.д. матриц из предлагаемой последовательности. На первом шаге заполняется строка, соответствующая сложности умножения двух матриц, взятых последовательно из предлагаемого произведения. На втором шаге рассчитываются минимальные сложности умножения трёх последовательных матриц, основываясь на известных минимальных сложностях умножения двух матриц. Заполняется соответствующая строка, и т.д.
// Программа рассчитывает матрицу минимальных сложностей умножения NUM матриц,
// размеры которых r0, r1, …, rn заданы в массиве Sizes
#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#define NUM 6 // Число перемножаемых матриц
#define NUM1 NUM+1 // Размер массива Sizes
unsigned Sizes[NUM1]; // Массив, содержащий размеры перемножаемых матриц
unsigned long Time_comp[(NUM1*NUM)>>1]; // Матрица минимальных сложностей умножения
void main()
{
unsigned long value, temp, *ptr, *current[NUM<<1];
unsigned ind, index, count, curr, max, min;
// Здесь должна стоять функция ввода массива Sizes
// Процедура заполнения матрицы Time_copm с помощью метода
// динамического программирования
ptr=Time_comp;
for(index=0; index<NUM; *ptr++=0, index++); // Первая строка заполняется нулями
for (ind=1; ind<NUM; ind++)
{
max=(ind<<1)-1;
// Предварительный расчёт указателей на элементы матрицы Time_copm, которые
// используются в дальнейшем при выборе минимальной сложности умножения.
// Указатели хранятся в массиве current.
for(index=0; index<ind; index++)
{
count=(index*((NUM<<1)-index+1))>>1;
curr=count+ind-index;
current[index<<1]=&Time_comp[count];
current[(index<<1)+1]=&Time_comp[curr];
}
for (index=1; index<NUM; index++)
{
count=ind+index;
if(count>NUM) break;
// Выбор минимальной сложности умножения заданного количества матриц
value=4294967294; // максимальное число типа long int
Рассмотрим характерный пример умножения некоторого количества матриц.
Пример 1.8. Предположим, что требуется найти произведение шести матриц
M = M1 ´ M2 ´ M3´ M4 ´ M5 ´ M6,
(15´7) (7´8) (8´46) (46´2) (2´39) (39´38)
соответствующие размеры которых указаны под матрицами. Здесь величины r0, r1, r2, r3, r4, r5, r6 соответственно равны 15, 7, 8, 46, 2, 39, 38. В результате работы алгоритма 7 получается матрица минимальных сложностей умножения, показанная в таблице 4. Из матрицы видно, что минимальная сложность умножения данных шести матриц равна 5162. Из этой же таблицы несложно получить и порядок умножения матриц. Для этого надо приписать к каждой клетке таблицы то значение k, на котором достигается минимум (1.11). Для удобства полученные значения k показаны в маленьких левых нижних клетках, а в верхних маленьких клетках показаны индексы i и j из (1.11).
Таблица 4. Сложности вычисления произведений Mi´Mi+1´…´Mj
0
0
0
0
0
0
840
2576
736
3588
2964
6360
848
1360
6224
1058
1394
4308
2228
4344
5162
Теперь рассмотрим последнюю клетку таблицы
5162
Цифры в левом верхнем углу i=1, j=6 показывают, что данная клетка показывает минимальную сложность умножения матриц с первой по шестую. Эта сложность равна 5162. Цифра k=4 показывает оптимальную расстановку скобок, а именно - нужно умножить сначала матрицы (M1´M2´…´M4), далее матрицы (M5´M6), после чего найти произведение полученных сомножителей. Чтобы найти оптимальный способ умножения (M1´M2´…´M4) необходимо найти в таблице ячейку с параметрами i=1, j=4. Число k=1 в этой ячейке указывает на то, что сначала необходимо найти произведение матриц (M2´M3´M4), а далее умножить его на M1. И так далее. В результате получается следующий способ оптимального умножения матриц с точки зрения сложности вычислений (и далеко не очевидный при самостоятельном поиске варианта расстановки скобок):
Mопт = (M1 ´ (M2 ´ (M3´ M4))) ´ (M5 ´ M6).
(15´7) (7´8) (8´46) (46´2) (2´39) (39´38)
Если теперь сравнить полученный результат с вариантом, когда матрицы умножаются без расстановки скобок последовательно в порядке записи
M = M1 ´ M2 ´ M3´ M4 ´ M5 ´ M6,
то получится сложность умножения, равная 31140. С другой стороны, если попробовать найти полученное оптимальное расположение скобок простым перебором всех возможных вариантов, то получится задача сложности зачастую превышающей сложность самой задачи умножения матриц.