Теория и практика параллельных вычислений

Параллельные методы на графах

Разбить на страницы
Показывать лекцию целиком

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

Пусть G есть граф

G=(V,R),

для которого набор вершин Vi, 0<=i<=n, задается множеством V, а список дуг графа$$r_j=(\nu_{s_j},\nu_{t_j}), \; 1 \le j \le m$$ определяется множеством R. В общем случае дугам графа могут приписываться некоторые числовые характеристики ( веса ) wj, 0<=j<=m (взвешенный граф ). Пример взвешенного графа приведен на рис. 10.1.

(рис 10.1) Пример взвешенного ориентированного графа

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

A=(aij), 1<=i, j<=n,

ненулевые значения элементов которой соответствуют дугам графа:$$a_{ij}= \left\{ \begin{aligned} w(\nu_i, \nu_j), \text{если } (\nu_i, \nu_j) \in R , \\ 0, \text{если } i=j, \\ \infty, \text{иначе} \end{aligned} \right.$$ (для обозначения отсутствия ребра между вершинами в матрице смежности на соответствующей позиции используется знак бесконечности, при вычислениях знак бесконечности может быть заменен, например, на любое отрицательное число). Так, например, матрица смежности, соответствующая графу на рис. 10.1, приведена на рис. 10.2.

(рис 10.2) Матрица смежности для графа с рис. 10.1

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

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

10.1. Задача поиска всех кратчайших путей

Исходной информацией для задачи является взвешенный граф G=(V,R), содержащий n вершин (|V|=n), в котором каждому ребру графа приписан неотрицательный вес. Граф будем полагать ориентированным, т.е. если из вершины i есть ребро в вершину j, то из этого не следует наличие ребра из j в i. В случае если вершины все же соединены взаимообратными ребрами, веса, приписанные им, могут не совпадать. Рассмотрим задачу, в которой для имеющегося графа G требуется найти минимальные длины путей между каждой парой вершин графа. В качестве практического примера можно привести задачу составления маршрута движения транспорта между различными городами при заданном расстоянии между населенными пунктами и другие подобные задачи.

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

10.1.1. Последовательный алгоритм Флойда

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

Алгоритм 10.1. Общая схема алгоритма Флойда

// Алгоритм 10.1
// Последовательный алгоритм Флойда
for (k = 0; k < n; k++)
  for (i = 0; i < n; i++)
    for (j = 0; j < n; j++)
      A[i, j] = min(A[i, j], A[i, k] + A[k, j]);

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

Дополнительная информация и доказательство правильности алгоритма Флойда могут быть получены, например, в работе [].

10.1.2. Разделение вычислений на независимые части

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

Покажем корректность такого способа организации параллелизма. Для этого нужно доказать, что операции обновления значений матрицы A на одной и той же итерации внешнего цикла k могут выполняться независимо. Иными словами, следует показать, что на итерации k не происходит изменения элементов Aik и Akj ни для одной пары индексов (i, j). Рассмотрим выражение, по которому происходит изменение элементов матрицы A:

Aij <- min (Aij, Aik + Akj).

Для i=k получим

Akj <- min (Akj, Akk + Akj),

но тогда значение Akj не изменится, т.к. Akk=0.

Для j=k выражение преобразуется к виду

Aik <- min (Aik, Aik + Akk),

что также показывает неизменность значений Aik. Как результат, необходимые условия для организации параллельных вычислений обеспечены, и, тем самым, в качестве базовой подзадачи может быть использована операция обновления элементов матрицы A (для указания подзадач будем применять индексы обновляемых в подзадачах элементов).

10.1.3. Выделение информационных зависимостей

Выполнение вычислений в подзадачах становится возможным только тогда, когда каждая подзадача (i, j) содержит необходимые для расчетов элементы Aij, Aik, Akj матрицы A. Для исключения дублирования данных разместим в подзадаче (i, j) единственный элемент Aij, тогда получение всех остальных необходимых значений может быть обеспечено только при помощи передачи данных. Таким образом, каждый элемент Akj строки k матрицы A должен быть передан всем подзадачам (k, j), 1<=j<=n, а каждый элемент Aik столбца k матрицы A должен быть передан всем подзадачам (i, k), 1<=i<=n,– см. рис. 10.3.

(рис 10.3) Информационная зависимость базовых подзадач (стрелками показаны направления обмена значениями на итерации k)

10.1.4. Масштабирование и распределение подзадач по процессорам

Как правило, число доступных процессоров p существенно меньше, чем число базовых задач n2 (p<<n2). Возможный способ укрупнения вычислений состоит в использовании ленточной схемы разбиения матрицы A – такой подход соответствует объединению в рамках одной базовой подзадачи вычислений, связанных с обновлением элементов одной или нескольких строк ( горизонтальное разбиение) или столбцов ( вертикальное разбиение) матрицы A. Эти два типа разбиения практически равноправны – учитывая дополнительный момент, что для алгоритмического языка C массивы располагаются по строкам, будем рассматривать далее только разбиение матрицы A на горизонтальные полосы.

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

10.1.5. Анализ эффективности параллельных вычислений

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

Общая трудоемкость последовательного алгоритма, как уже отмечалось ранее, имеет порядок сложности n3. Для параллельного алгоритма на отдельной итерации каждый процессор выполняет обновление элементов матрицы А. Всего в подзадачах n2/p таких элементов, число итераций алгоритма равно n – таким образом, показатели ускорения и эффективности параллельного алгоритма Флойда имеют вид:$$S_p=\frac{n^3}{(n^3/p)}=p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$

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

Коммуникационная операция, выполняемая на каждой итерации алгоритма Флойда, состоит в передаче одной из строк матрицы А всем процессорам вычислительной системы. Как уже показывалось ранее, такая операция может быть выполнена за $$\lceil log_{2}p\rceil$$ шагов. С учетом количества итераций алгоритма Флойда при использовании модели Хокни общая длительность выполнения коммуникационных операций может быть определена при помощи следующего выражения$$T_p(comm)=n\lceil\log_2p\rceil(\alpha+wn/ \beta),$$ где, как и ранее, $$\alpha$$ – латентность сети передачи данных, $$\beta$$ – пропускная способность сети, а w есть размер элемента матрицы в байтах.

С учетом полученных соотношений общее время выполнения параллельного алгоритма Флойда может быть определено следующим образом:$$T_p=n^2\cdot\lceil n/p\rceil\cdot\tau+n\cdot\lceil\log_2p\rceil(\alpha+w\cdot n/ \beta),$$ где $$\tau$$ есть время выполнения операции выбора минимального значения.

10.1.6. Программная реализация

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

1. Главная функция программы. Реализует логику работы алгоритма, последовательно вызывает необходимые подпрограммы.

// Программа 10.1. — Алгоритм Флойда
int ProcRank;    // Ранг текущего процесса
int ProcNum;     // Количество процессов

// Функция вычисления минимума
int Min(int A, int B) {
  int Result = (A < B) ? A : B;

  if((A < 0)  (B >= 0)) Result = B;
  if((B < 0)  (A >= 0)) Result = A;
  if((A < 0)  (B < 0))  Result = -1;

  return Result;
}

// Главная функция программы
int main(int argc, char* argv[]) {
  int *pMatrix;     	// Матрица смежности
  int  Size;        	// Размер матрицы смежности
  int *pProcRows;   	// Строки матрицы смежности текущего процесса
  int  RowNum;      	// Число строк для текущего процесса

  MPI_Init(argc, argv);
  MPI_Comm_size(MPI_COMM_WORLD, ProcNum);
  MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);

  // Инициализация данных
  ProcessInitialization(pMatrix, pProcRows, Size, RowNum);

  // Распределение данных между процессами
  DataDistribution(pMatrix, pProcRows, Size, RowNum);

  // Параллельный алгоритм Флойда
  ParallelFloyd(pProcRows, Size, RowNum);

  // Сбор результатов работы алгоритма
  ResultCollection(pMatrix, pProcRows, Size, RowNum);

  // Завершение вычислений процесса
  ProcessTermination(pMatrix, pProcRows);

  MPI_Finalize();
  return 0;
}

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

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

Функция DataDistribution распределяет исходные данные между процессами. Каждый процесс получает горизонтальную полосу матрицы смежности.

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

Функция ProcessTermination выполняет необходимый вывод результатов решения задачи и освобождает всю ранее выделенную память для хранения данных.

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

2. Функция ParallelFloyd. Данная функция осуществляет итеративное изменение матрицы смежности в соответствии с алгоритмом Флойда.

// Параллельный алгоритм Флойда
void ParallelFloyd(int *pProcRows, int Size, int RowNum) {
  int *pRow = new int[Size];
  int t1, t2;

  for(int k = 0; k < Size; k++) {
    // Распределение k-й строки среди процессов 
    RowDistribution(pProcRows, Size, RowNum, k, pRow);

    // Обновление элементов матрицы смежности
    for(int i = 0; i < RowNum; i++) 
      for(int j = 0; j < Size; j++)
        if( (pProcRows[i * Size + k] != -1)  (pRow [j]!= -1)){
          t1 = pProcRows[i * Size + j];
          t2 = pProcRows[i * Size + k] + pRow[j];
          pProcRows[i * Size + j] = Min(t1, t2);
        }
  }
    
  delete []pRow;
}

3. Функция RowDistribution. Данная функция рассылает k -ю строку матрицы смежности всем процессам программы.

// Функция для рассылки строки всем процессам
void RowDistribution(int *pProcRows, int Size, int RowNum, int k,
  int *pRow) {
  int ProcRowRank; 	// Ранг процесса, которому принадлежит k-я строка
  int ProcRowNum;	// Номер k-й строки в полосе матрицы

  // Нахождение ранга процесса – владельца k-й строки
  int RestRows = Size;
  int Ind = 0;
  int Num = Size / ProcNum;
  
  for(ProcRowRank = 1; ProcRowRank < ProcNum + 1; ProcRowRank ++)      {
    if(k < Ind + Num ) break;
    RestRows -= Num;
    Ind      += Num;
    Num       = RestRows / (ProcNum - ProcRowRank);
  }
  ProcRowRank = ProcRowRank - 1;
  ProcRowNum  = k - Ind;

  if(ProcRowRank == ProcRank)
    // Копирование строки в массив pRow
    copy(pProcRows[ProcRowNum * Size], pProcRows[(ProcRowNum + 1) *
      Size], pRow);

  // Распределение k-й строки между процессами  
  MPI_Bcast(pRow, Size, MPI_INT, ProcRowRank, MPI_COMM_WORLD);
}

10.1.7. Результаты вычислительных экспериментов

Эксперименты проводились на вычислительном кластере Нижегородского университета на базе процессоров Intel Xeon 4 EM64T, 3000 МГц и сети Gigabit Ethernet под управлением операционной системы Microsoft Windows Server 2003 Standard x64 Edition и системы управления кластером Microsoft Compute Cluster Server.

Для оценки длительности $$\tau$$ базовой скалярной операции выбора минимального значения проводилось решение задачи поиска всех кратчайших путей при помощи последовательного алгоритма и полученное таким образом время вычислений делилось на общее количество выполненных операций – в результате для величины $$\tau$$ было получено значение 7,14 нсек. Эксперименты, выполненные для определения параметров сети передачи данных, показали значения латентности $$\alpha$$ и пропускной способности $$\beta$$ соответственно 130 мкс и 53,29 Мбайт/с. Все вычисления производились над числовыми значениями типа int, размер которого на данной платформе равен 4 байта (следовательно, w=4 ).

Результаты вычислительных экспериментов приведены в таблице 10.1. Эксперименты выполнялись с использованием двух, четырех и восьми процессоров. Время указано в секундах.

Результаты вычислительных экспериментов для параллельного алгоритма Флойда
Кол-во вершин Последовательный алгоритм Параллельный алгоритм
2 процессора 4 процессора 8 процессоров
Время Ускорение Время Ускорение Время Ускорение
1000 8,037 4,152 1,936 2,067 3,888 0,941 8,544
2000 59,812 30,323 1,972 15,375 3,890 8,058 7,423
3000 197,111 99,264 1,986 50,232 3,924 25,643 7,687
4000 461,785 232,507 1,986 117,220 3,939 69,946 6,602
5000 884,622 443,747 1,994 224,441 3,941 128,078 6,907
(рис 10.4) Графики зависимости ускорения параллельного алгоритма Флойда от числа используемых процессоров при различном количестве вершин графа

Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретической оценки Tp из (10.3) приведено в таблице 10.2 и на рис. 10.5.

(рис 10.5) Графики экспериментально установленного времени работы параллельного алгоритма Флойда и теоретической оценки в зависимости от количества вершин графа при использовании двух процессоров
Сравнение экспериментального и теоретического времени работы алгоритма Флойда
Количество вершин Последовательный алгоритм Параллельный алгоритм
2 процессора 4 процессора 8 процессоров
$$T_1^*$$ $$T_2$$ $$T_2^*$$ $$T_4$$ $$T_4^*$$ $$T_8$$ $$T_8^*$$
1000 8,038 3,776 4,152 2,196 2,067 1,509 0,941
2000 59,812 29,123 30,323 15,405 15,375 8,826 8,058
3000 197,111 97,465 99,264 50,336 50,232 27,307 25,643
4000 461,785 230,220 232,507 117,701 117,220 62,306 69,946
5000 884,622 448,811 443,747 228,211 224,441 119,179 128,078

10.2. Задача нахождения минимального охватывающего дерева

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

(рис 10.6) Пример взвешенного неориентированного графа (а) и соответствующему ему минимально охватывающего дерева (б)

Дадим общее описание алгоритма решения поставленной задачи, известного под названием метода Прима ( the Prim method ); более полная информация может быть получена, например, в [].

10.2.1. Последовательный алгоритм Прима

Алгоритм начинает работу с произвольной вершины графа, выбираемой в качестве корня дерева, и в ходе последовательно выполняемых итераций расширяет конструируемое дерево до МОД. Пусть VT есть множество вершин, уже включенных алгоритмом в МОД, а величины di, 1<=i<=n, характеризуют дуги минимальной длины от вершин, еще не включенных в дерево, до множества VT, т.е.$$\forall i \notin V_T \Rightarrow d_i = \min\{ w(i,u):u\in V_T,(i,u)\in R\}$$ (если для какой-либо вершины $$i\notin V_{T}$$ не существует ни одной дуги в VT, значение di устанавливается равным $$\infty$$ ). В начале работы алгоритма выбирается корневая вершина МОД s и полагается VT={s}, ds=0.

Действия, выполняемые на каждой итерации алгоритма Прима, состоят в следующем:

  • определяются значения величин di для всех вершин, еще не включенных в состав МОД;
  • выбирается вершина t графа G, имеющая дугу минимального веса до множества $$V_{T}t:d_{t},\ i\notin V_{T}$$ ;
  • вершина t включается в VT.
  • После выполнения n-1 итерации метода МОД будет сформировано. Вес этого дерева может быть получен при помощи выражения$$W_T = \sum_{i=1}^n d_i.$$

    Трудоемкость нахождения МОД характеризуется квадратичной зависимостью от числа вершин графа T1~n2.

    10.2.2. Разделение вычислений на независимые части

    Оценим возможности параллельного выполнения рассмотренного алгоритма нахождения минимально охватывающего дерева.

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

    Распределение данных между процессорами вычислительной системы должно обеспечивать независимость перечисленных операций алгоритма Прима. В частности, это может быть реализовано, если каждая вершина графа располагается на процессоре вместе со всей связанной с вершиной информацией. Соблюдение данного принципа приводит к тому, что при равномерной загрузке каждый процессор pj, 1<=j<=p, должен содержать:

  • набор вершин$$V_j=\{\nu_{i_j +1}, \nu_{i_j +2}, \ldots, \nu_{i_j +k} \}, \; i_j = k\cdot(j-1), k=\lceil n/p\rceil;$$
  • соответствующий этому набору блок из k величин$$\Delta_j = \{d_{i_j +1},d_{i_j +2},\ldots,d_{i_j +k}\};$$
  • вертикальную полосу матрицы смежности графа G из k соседних столбцов$$A_j=\{\alpha_{i_j +1},\alpha_{i_j +2},\ldots,\alpha_{i_j +k}\} (\alpha_s \text{ есть } s\text{-й столбец матрицы } A);$$
  • общую часть набора Vj и формируемого в процессе вычислений множества вершин VT.
  • Как итог можем заключить, что базовой подзадачей в параллельном алгоритме Прима может служить процедура вычисления блока значений $$\Delta _{j}$$ для вершин Vj матрицы смежности A графа G.

    10.2.3. Выделение информационных зависимостей

    С учетом выбора базовых подзадач общая схема параллельного выполнения алгоритма Прима будет состоять в следующем:

  • определяется вершина t графа G, имеющая дугу минимального веса до множества VT. Для выбора такой вершины необходимо осуществить поиск минимума в наборах величин di, имеющихся на каждом из процессоров, и выполнить сборку полученных значений на одном из процессоров;
  • номер выбранной вершины для включения в охватывающее дерево передается всем процессорам;
  • обновляются наборы величин di с учетом добавления новой вершины.
  • Таким образом, в ходе параллельных вычислений между процессорами выполняются два типа информационных взаимодействий: сбор данных от всех процессоров на одном из процессоров и передача сообщений от одного процессора всем процессорам вычислительной системы.

    10.2.4. Масштабирование и распределение подзадач по процессорам

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

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

    10.2.5. Анализ эффективности параллельных вычислений

    Общий анализ сложности параллельного алгоритма Прима для нахождения минимального охватывающего дерева дает идеальные показатели эффективности параллельных вычислений:$$S_p = \frac{n^2}{(n^2/p)}=p \quad \text{и} \quad E_p = \frac{n^2}{p\cdot(n^2/p)}=1.$$

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

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

    При выполнении вычислений на отдельной итерации параллельного алгоритма Прима каждый процессор определяет номер ближайшей вершины из Vj до охватывающего дерева и осуществляет корректировку расстояний di после расширения МОД. Количество выполняемых операций в каждой из этих вычислительных процедур ограничивается сверху числом вершин, имеющихся на процессорах, т.е. величиной $$\lceil n/p\rceil$$. Как результат, с учетом общего количества итераций n время выполнения вычислительных операций параллельного алгоритма Прима может быть оценено при помощи соотношения:$$T_p(calc)=2n\lceil n/p \rceil \cdot \tau$$ (здесь, как и ранее, $$\tau$$ есть время выполнения одной элементарной скалярной операции).

    Операция сбора данных от всех процессоров на одном из процессоров может быть произведена за $$\lceil log_{2}p\rceil$$ итераций, при этом общая оценка длительности выполнения передачи данных определяется выражением (более подробное рассмотрение данной коммуникационной операции содержится в лекции 3):$$T_p^1(comm)=n(\alpha \log_2 p +3w(p-1)/ \beta),$$ где $$\alpha$$ – латентность сети передачи данных, $$\beta$$ – пропускная способность сети, а w есть размер одного пересылаемого элемента данных в байтах (коэффициент 3 в выражении соответствует числу передаваемых значений между процессорами – длина минимальной дуги и номера двух вершин, которые соединяются этой дугой).

    Коммуникационная операция передачи данных от одного процессора всем процессорам вычислительной системы также может быть выполнена за $$\lceil log_{2}p\rceil$$ итераций при общей оценке времени выполнения вида:$$T_p^2(comm)=n\log_2p(\alpha+w/ \beta)$$

    С учетом всех полученных соотношений общее время выполнения параллельного алгоритма Прима составляет:$$T_p=2n\lceil n/p\rceil\cdot\tau+n(\alpha\cdot\log_2p+3w(p-1)/ \beta+log_2p(\alpha+w/ \beta)).$$

    10.2.6. Результаты вычислительных экспериментов

    Вычислительные эксперименты для оценки эффективности параллельного алгоритма Прима осуществлялись при тех же условиях, что и ранее выполненные (см. п. 10.1.7).

    Для оценки длительности $$\tau$$ базовой скалярной операции проводилось решение задачи нахождения минимального охватывающего дерева при помощи последовательного алгоритма и полученное таким образом время вычислений делилось на общее количество выполненных операций – в результате подобных экспериментов для величины $$\tau$$ было получено значение 4,76 нсек. Все вычисления производились над числовыми значениями типа int, размер которого на данной платформе равен 4 байта (следовательно, w=4 ).

    Результаты вычислительных экспериментов даны в таблице 10.3. Эксперименты проводились с использованием двух, четырех и восьми процессоров. Время указано в секундах.

    Результаты вычислительных экспериментов для параллельного алгоритма Прима
    Кол-во вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    Время Ускорение Время Ускорение Время Ускорение
    1000 0,044 0,248 0,176 0,932 0,047 1,574 0,028
    2000 0,208 0,684 0,304 1,800 0,115 2,159 0,096
    3000 0,485 1,403 0,346 2,214 0,219 3,195 0,152
    4000 0,873 1,946 0,622 3,324 0,263 5,431 0,161
    5000 1,432 2,665 0,736 2,933 0,488 4,119 0,348
    6000 2,189 2,900 0,821 4,291 0,510 7,737 0,283
    7000 3,042 3,236 0,940 6,327 0,481 8,825 0,345
    8000 4,150 4,462 0,930 6,993 0,593 10,390 0,399
    9000 5,622 5,834 0,964 7,475 0,752 10,764 0,522
    10000 7,512 6,990 1,075 8,597 0,874 14,095 0,533
    (рис 10.7) Графики зависимости ускорения параллельного алгоритма Прима от числа используемых процессоров при различном количестве вершин в модели

    Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретической оценки Tp из (10.8) приведено в таблице 10.4 и на рис. 10.8.

    Сравнение экспериментального и теоретического времени работы алгоритма Прима
    Количество вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    $$T_1^*$$ $$T_2$$ $$T_2^*$$ $$T_4$$ $$T_4^*$$ $$T_8$$ $$T_8^*$$
    1000 0,044 0,405 0,248 0,804 0,932 1,205 1,574
    2000 0,208 0,820 0,684 1,613 1,800 2,412 2,159
    3000 0,485 1,245 1,403 2,426 2,214 3,622 3,195
    4000 0,873 1,679 1,946 3,245 3,324 4,834 5,431
    5000 1,432 2,122 2,665 4,068 2,933 6,048 4,119
    6000 2,189 2,575 2,900 4,896 4,291 7,265 7,737
    7000 3,042 3,038 3,236 5,728 6,327 8,484 8,825
    8000 4,150 3,510 4,462 6,566 6,993 9,705 10,390
    9000 5,622 3,991 5,834 7,408 7,475 10,929 10,764
    10000 7,512 4,482 6,990 8,255 8,597 12,155 14,095
    (рис 10.8) Графики экспериментально установленного времени работы параллельного алгоритма Прима и теоретической оценки в зависимости от количества вершин в модели при использовании двух процессоров

    Как можно заметить из табл. 10.4 и рис. 10.8, теоретические оценки определяют время выполнения алгоритма Прима с достаточно высокой погрешностью. Причина такого расхождения может состоять в том, что модель Хокни менее точна при оценке времени передачи сообщений с небольшим объемом передаваемых данных. Для уточнения получаемых оценок необходимым является использование других более точных моделей расчета трудоемкости коммуникационных операций – обсуждение этого вопроса проведено в лекции 3.

    10.3. Задача оптимального разделения графов

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

    (рис 10.9) Пример разделения нерегулярной сети

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

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

    (рис 10.10) Пример графа, моделирующего структуру сети на рис. 10.9

    Дополнительная информация по проблеме разделения графов может быть получена, например, в [].

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

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

    Пусть дан взвешенный неориентированный граф G=(V,E), каждой вершине $$\nu \in V$$ и каждому ребру $$e\in E$$ которого приписан вес. Задача оптимального разделения графа состоит в разбиении его вершин на непересекающиеся подмножества с максимально близкими суммарными весами вершин и минимальным суммарным весом ребер, проходящих между полученными подмножествами вершин.

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

    Далее для простоты изложения учебного материала будем полагать веса вершин и ребер графа равными единице.

    10.3.2. Метод рекурсивного деления пополам

    Для решения задачи разбиения графа можно рекурсивно применить метод бинарного деления ( the binary bisection method ), при котором на первой итерации граф разделяется на две равные части, далее на втором шаге каждая из полученных частей также разбивается на две части и т. д. В данном подходе для разбиения графа на k частей необходимо log2k уровней рекурсии и выполнение k-1 деления пополам. В случае когда требуемое количество разбиений k не является степенью двойки, каждое деление пополам необходимо осуществлять в соответствующем соотношении.

    Поясним схему работы метода деления пополам на примере разделения графа на рис. 10.11 на 5 частей. Сначала граф следует разделить на 2 части в оотношении 2:3 (непрерывная линия), затем правую часть разбиения – в отношении 1:3 (штриховая линия), после этого осталось разделить 2 крайние подобласти слева и справа в отношении 1:1 (штрих-пунктир).

    (рис 10.11) Пример разбиения графа на 5 частей методом рекурсивного деления пополам

    10.3.3. Геометрические методы

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

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

    10.3.3.1. Покоординатное разбиение

    Покоординатное разбиение ( the coordinate nested dissection ) – это метод, основанный на рекурсивном делении пополам сети по наиболее длинной стороне. В качестве иллюстрации на рис. 10.12 показан пример сети, при разделении которой именно такой способ разбиения дает существенно меньшее количество информационных связей между разделенными частями, по сравнению со случаем, когда сеть делится по меньшей (вертикальной) стороне.

    (рис 10.12) Пример разделения сети графическим методом по наибольшей размерности (граница раздела показана жирной линией)

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

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

    10.3.3.2. Рекурсивный инерционный метод деления пополам

    Предыдущая схема могла производить разбиение сети только по линии, перпендикулярной одной из координатных осей. Во многих случаях такое ограничение оказывается критичным для построения качественного разбиения. Достаточно повернуть сеть на рис. 10.12 под острым углом к координатным осям (см. рис. 10.13), чтобы убедиться в этом. Для минимизации границы между подсетями желательна возможность проведения линии разделения с любым требуемым углом поворота. Возможный способ определения угла поворота, используемый в рекурсивном инерционном методе деления пополам ( the recursive inertial bisection ), состоит в использовании главной инерционной оси (см., например, []), считая элементы сети точечными массами. Линия бисекции, ортогональная полученной оси, как правило, дает границу наименьшей длины.

    (рис 10.13) Пример разделения сети методом рекурсивной инерционной бисекции. Стрелкой показана главная инерционная ось

    10.3.3.3. Деление сети с использованием кривых Пеано

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

    Один из таких методов упорядочивает элементы в соответствии с позициями центров их масс вдоль кривых Пеано. Кривые Пеано – это кривые, полностью заполняющие области больших размерностей (например, квадрат или куб). Применение таких кривых обеспечивает близость точек фигуры, которые соответствуют точкам, близким на кривой. После получения списка элементов сети, упорядоченного в зависимости от расположения на кривой, достаточно разделить список на необходимое число частей в соответствии с установленным порядком. Получаемый в результате такого подхода метод носит в литературе наименование алгоритма деления сети с использованием кривых Пеано ( the space-filling curve technique ). Подробнее о методе можно прочитать в работах [, , ].

    (рис 10.14) Пример разделения сети на 3 части с использованием кривых Пеано

    10.3.4. Комбинаторные методы

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

    10.3.4.1. Деление с учетом связности

    С самых общих позиций понятно, что при разделении графа информационная зависимость между разделенными подграфами будет меньше, если соседние вершины (вершины, между которыми имеются дуги) будут находиться в одном подграфе. Алгоритм деления графов с учетом связности ( the levelized nested dissection algorithm ) пытается достичь этого, последовательно добавляя к формируемому подграфу соседей. На каждой итерации алгоритма происходит разделение графа на 2 части. Таким образом, разделение графа на требуемое число частей достигается путем рекурсивного применения алгоритма.

    Общая схема алгоритма может быть описана при помощи следующего набора правил.

    Алгоритм 10.2. Общая схема выполнения алгоритма деления графов с учетом связности

  • Iteration = 0.
  • Присвоение номера Iteration произвольной вершине графа.
  • Присвоение ненумерованным соседям вершин с номером Iteration номера Iteration + 1.
  • Iteration = Iteration + 1.
  • Если еще есть неперенумерованные соседи, то переход на шаг 3.
  • Разделение графа на 2 части в порядке нумерации.
  • Для минимизации информационных зависимостей имеет смысл в качестве начальной выбирать граничную вершину. Поиск такой вершины можно осуществить методом, близким к рассмотренной схеме. Так, перенумеровав вершины графа в соответствии с алгоритмом 10.2 (начиная нумерацию из произвольной вершины), мы можем взять любую вершину с максимальным номером. Как нетрудно убедиться, она будет граничной.

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

    (рис 10.15) Пример работы алгоритма деления графов с учетом связности

    10.3.4.2. Алгоритм Кернигана – Лина

    В алгоритме Кернигана – Лина ( the Kernighan – Lin algorithm ) используется несколько иной подход для решения проблемы оптимального разбиения графа – предполагается, что некоторое начальное разбиение графа уже существует, затем имеющееся приближение улучшается в течение некоторого количества итераций. Применяемый способ улучшения в алгоритме Кернигана – Лина состоит в обмене вершинами между подмножествами имеющегося разбиения графа (см. рис. 10.16). Для формирования требуемого количества частей графа может быть использована, как и ранее, рекурсивная процедура деления пополам.

    Общая схема одной итерации алгоритма Кернигана – Лина может быть представлена следующим образом.

    Алгоритм 10.3. Общая схема алгоритма Кернигана – Лина

  • Формирование множества пар вершин для перестановки. Из вершин, которые еще не были переставлены на данной итерации, формируются все возможные пары (в парах должно присутствовать по одной вершине из каждой части имеющегося разбиения графа ).
  • Построение новых вариантов разбиения графа. Каждая пара, подготовленная на шаге 1, поочередно используется для обмена вершин между частями имеющегося разбиения графа для получения множества новых вариантов деления.
  • Выбор лучшего варианта разбиения графа. Для сформированного на шаге 2 множества новых делений графа выбирается лучший вариант. Этот вариант далее фиксируется как новое текущее разбиение графа, а соответствующая выбранному варианту пара вершин отмечается как использованная на текущей итерации алгоритма.
  • Проверка использования всех вершин. При наличии в графе вершин, еще не использованных при перестановках, выполнение итерации алгоритма снова продолжается с шага 1. Если же перебор вершин графа завершен, далее следует шаг 5.
  • Выбор наилучшего варианта разбиения графа. Среди всех разбиений графа, полученных на шаге 3 проведенных итераций, выбирается (и фиксируется) наилучший вариант разбиения графа.
  • Поясним дополнительно, что на шаге 2 итерации алгоритма перестановка вершин каждой очередной пары осуществляется для одного и того же разбиения графа, выбранного до начала выполнения итерации или определенного на шаге 3. Общее количество выполняемых итераций, как правило, фиксируется заранее и является параметром алгоритма (за исключением случая остановки при отсутствии улучшения разбиения на очередной итерации).

    (рис 10.16) Пример перестановки двух вершин (выделены серым) в методе Кернигана – Лина

    10.3.5. Сравнение алгоритмов разбиения графов

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

    Сравнительная таблица некоторых алгоритмов разделения графов
    Алгоритмы Необходимость координатной информации Точность Время выполнения Возможности для распараллеливания
    Покоординатное разбиение Да $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Рекурсивный инерционный метод деления пополам Да $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Деление с учетом связности Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Алгоритм Кернигана - Лина 1 итерация Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    10 итераций Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    50 итераций Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$

    Столбец "Необходимость координатной информации" отмечает использование алгоритмом координатной информации об элементах сети или вершинах графа.

    Столбец "Точность" дает качественную характеристику величины приближения получаемых алгоритмом решений к оптимальным вариантам разбиения графов. Каждый дополнительный закрашенный кружок определяет примерно 10%-процентное улучшение точности приближения.

    Столбец "Время выполнения" показывает относительное время, затрачиваемое различными алгоритмами разбиения. Каждый дополнительный закрашенный кружок соответствует увеличению времени разбиения примерно в 10 раз.

    Столбец "Возможности для распараллеливания" характеризует свойства алгоритмов для параллельного выполнения. Алгоритм Кернигана – Лина при выполнении только одной итерации почти не поддается распараллеливанию. Этот же алгоритм при большем количестве итераций, а также метод деления с учетом связности могут быть распараллелены со средней эффективностью. Алгоритм покоординатного разбиения и рекурсивный инерционный метод деления пополам обладают высокими показателями для распараллеливания.

    10.4. Краткий обзор лекции

    В лекции рассмотрен ряд алгоритмов для решения типовых задач обработки графов. Кроме того, приведен обзор методов разделения графа.

    В подразделе 10.1 представлен алгоритм Флойда ( the Floyd algorithm ) – дается общая вычислительная схема последовательного варианта метода, обсуждаются способы его распараллеливания, проводится анализ эффективности получаемых параллельных вычислений, рассматривается программная реализация метода и приводятся результаты вычислительных экспериментов. Используемый подход к распараллеливанию алгоритма Флойда состоит в разделении вершин графа между процессорами, а необходимое при этом информационное взаимодействие состоит в передаче одной строки матрицы смежности от одного процессора всем процессорам вычислительной системы на каждой итерации метода.

    В подразделе 10.2 рассматривается алгоритм Прима ( the Prim algorithm ) для решения задачи поиска минимального охватывающего дерева (остова) неориентированного взвешенного графа. Остовом графа называют связный подграф без циклов (дерево), который содержит все вершины исходного графа и ребра, имеющие минимальный суммарный вес. Для алгоритма дается общее описание его исходного последовательного варианта, определяются возможные способы его параллельного выполнения, теоретические оценки ускорения и эффективности параллельных вычислений ; также рассматриваются результаты проведенных вычислительных экспериментов. Параллельный вариант алгоритма Прима, как и в предыдущем случае, основывается на разделении вершин графа между процессорами при несколько большем объеме информационных взаимодействий – на каждой итерации алгоритма необходимой является операция сбора данных на одном процессоре и последующая рассылка номера выбранной вершины графа всем процессорам вычислительной системы.

    Рассматриваемая в подразделе 10.3 задача оптимального разделения графов является важной для многих научных исследований, использующих параллельные вычисления. Для примера в подразделе приведен общий способ перехода от двумерной или трехмерной сети, моделирующей процесс вычислений, к соответствующему ей графу. Для решения задачи разбиения графов были рассмотрены геометрические методы, использующие при разделении сетей только координатную информацию об узлах сети, и комбинаторные алгоритмы, руководствующиеся смежностью вершин графа. К числу рассмотренных геометрических методов относятся покоординатное разбиение ( the coordinate nested dissection method ), рекурсивный инерционный метод деления пополам ( the recursive inertial bisection method ), деление сети с использованием кривых Пеано ( the space-filling curve techniques ). К числу рассмотренных комбинаторных алгоритмов относятся деление с учетом связности ( the levelized nested dissection ) и алгоритм Кернигана – Лина ( the Kernighan – Lin algorithm ). Для сопоставления рассмотренных подходов приводится общая сравнительная характеристика алгоритмов по времени выполнения, точности получаемого решения, возможностям для распараллеливания и т.п.

    10.5. Обзор литературы

    Дополнительная информация по алгоритмам Флойда и Прима может быть получена, например, в [].

    Подробное рассмотрение вопросов, связанных с проблемой разделения графов, содержится в работах [, , , , , , , , , ].

    Параллельные алгоритмы разделения графов рассматриваются в [, , , , , , ].

    10.6. Контрольные вопросы

  • Приведите определение графа. Какие основные способы используются для задания графов?
  • В чем состоит задача поиска всех кратчайших путей?
  • Приведите общую схему алгоритма Флойда. Какова трудоемкость алгоритма?
  • В чем состоит способ распараллеливания алгоритма Флойда?
  • В чем заключается задача нахождения минимального охватывающего дерева? Приведите пример использования задачи на практике.
  • Приведите общую схему алгоритма Прима. Какова трудоемкость алгоритма?
  • В чем состоит способ распараллеливания алгоритма Прима?
  • В чем отличие геометрических и комбинаторных методов разделения графа? Какие методы являются более предпочтительными? Почему?
  • Приведите описание метода покоординатного разбиения и алгоритма разделения с учетом связности. Какой из этих методов является более простым для реализации?
  • 10.7. Задачи и упражнения

  • Используя приведенный программный код, выполните реализацию параллельного алгоритма Флойда. Проведите вычислительные эксперименты. Постройте теоретические оценки с учетом параметров используемой вычислительной системы. Сравните полученные оценки с экспериментальными данными.
  • Выполните реализацию параллельного алгоритма Прима. Проведите вычислительные эксперименты. Постройте теоретические оценки с учетом параметров используемой вычислительной системы. Сравните полученные оценки с экспериментальными данными.
  • Разработайте программную реализацию алгоритма Кернигана – Лина. Дайте оценку возможности распараллеливания этого алгоритма.
  • Страницы:

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

    Пусть G есть граф

    G=(V,R),

    для которого набор вершин Vi, 0<=i<=n, задается множеством V, а список дуг графа$$r_j=(\nu_{s_j},\nu_{t_j}), \; 1 \le j \le m$$ определяется множеством R. В общем случае дугам графа могут приписываться некоторые числовые характеристики ( веса ) wj, 0<=j<=m (взвешенный граф ). Пример взвешенного графа приведен на рис. 10.1.

    (рис 10.1) Пример взвешенного ориентированного графа

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

    A=(aij), 1<=i, j<=n,

    ненулевые значения элементов которой соответствуют дугам графа:$$a_{ij}= \left\{ \begin{aligned} w(\nu_i, \nu_j), \text{если } (\nu_i, \nu_j) \in R , \\ 0, \text{если } i=j, \\ \infty, \text{иначе} \end{aligned} \right.$$ (для обозначения отсутствия ребра между вершинами в матрице смежности на соответствующей позиции используется знак бесконечности, при вычислениях знак бесконечности может быть заменен, например, на любое отрицательное число). Так, например, матрица смежности, соответствующая графу на рис. 10.1, приведена на рис. 10.2.

    (рис 10.2) Матрица смежности для графа с рис. 10.1

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

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

    10.1. Задача поиска всех кратчайших путей

    Исходной информацией для задачи является взвешенный граф G=(V,R), содержащий n вершин (|V|=n), в котором каждому ребру графа приписан неотрицательный вес. Граф будем полагать ориентированным, т.е. если из вершины i есть ребро в вершину j, то из этого не следует наличие ребра из j в i. В случае если вершины все же соединены взаимообратными ребрами, веса, приписанные им, могут не совпадать. Рассмотрим задачу, в которой для имеющегося графа G требуется найти минимальные длины путей между каждой парой вершин графа. В качестве практического примера можно привести задачу составления маршрута движения транспорта между различными городами при заданном расстоянии между населенными пунктами и другие подобные задачи.

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

    10.1.1. Последовательный алгоритм Флойда

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

    Алгоритм 10.1. Общая схема алгоритма Флойда

    // Алгоритм 10.1
    // Последовательный алгоритм Флойда
    for (k = 0; k < n; k++)
      for (i = 0; i < n; i++)
        for (j = 0; j < n; j++)
          A[i, j] = min(A[i, j], A[i, k] + A[k, j]);

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

    Дополнительная информация и доказательство правильности алгоритма Флойда могут быть получены, например, в работе [].

    10.1.2. Разделение вычислений на независимые части

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

    Покажем корректность такого способа организации параллелизма. Для этого нужно доказать, что операции обновления значений матрицы A на одной и той же итерации внешнего цикла k могут выполняться независимо. Иными словами, следует показать, что на итерации k не происходит изменения элементов Aik и Akj ни для одной пары индексов (i, j). Рассмотрим выражение, по которому происходит изменение элементов матрицы A:

    Aij <- min (Aij, Aik + Akj).

    Для i=k получим

    Akj <- min (Akj, Akk + Akj),

    но тогда значение Akj не изменится, т.к. Akk=0.

    Для j=k выражение преобразуется к виду

    Aik <- min (Aik, Aik + Akk),

    что также показывает неизменность значений Aik. Как результат, необходимые условия для организации параллельных вычислений обеспечены, и, тем самым, в качестве базовой подзадачи может быть использована операция обновления элементов матрицы A (для указания подзадач будем применять индексы обновляемых в подзадачах элементов).

    10.1.3. Выделение информационных зависимостей

    Выполнение вычислений в подзадачах становится возможным только тогда, когда каждая подзадача (i, j) содержит необходимые для расчетов элементы Aij, Aik, Akj матрицы A. Для исключения дублирования данных разместим в подзадаче (i, j) единственный элемент Aij, тогда получение всех остальных необходимых значений может быть обеспечено только при помощи передачи данных. Таким образом, каждый элемент Akj строки k матрицы A должен быть передан всем подзадачам (k, j), 1<=j<=n, а каждый элемент Aik столбца k матрицы A должен быть передан всем подзадачам (i, k), 1<=i<=n,– см. рис. 10.3.

    (рис 10.3) Информационная зависимость базовых подзадач (стрелками показаны направления обмена значениями на итерации k)

    10.1.4. Масштабирование и распределение подзадач по процессорам

    Как правило, число доступных процессоров p существенно меньше, чем число базовых задач n2 (p<<n2). Возможный способ укрупнения вычислений состоит в использовании ленточной схемы разбиения матрицы A – такой подход соответствует объединению в рамках одной базовой подзадачи вычислений, связанных с обновлением элементов одной или нескольких строк ( горизонтальное разбиение) или столбцов ( вертикальное разбиение) матрицы A. Эти два типа разбиения практически равноправны – учитывая дополнительный момент, что для алгоритмического языка C массивы располагаются по строкам, будем рассматривать далее только разбиение матрицы A на горизонтальные полосы.

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

    10.1.5. Анализ эффективности параллельных вычислений

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

    Общая трудоемкость последовательного алгоритма, как уже отмечалось ранее, имеет порядок сложности n3. Для параллельного алгоритма на отдельной итерации каждый процессор выполняет обновление элементов матрицы А. Всего в подзадачах n2/p таких элементов, число итераций алгоритма равно n – таким образом, показатели ускорения и эффективности параллельного алгоритма Флойда имеют вид:$$S_p=\frac{n^3}{(n^3/p)}=p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$

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

    Коммуникационная операция, выполняемая на каждой итерации алгоритма Флойда, состоит в передаче одной из строк матрицы А всем процессорам вычислительной системы. Как уже показывалось ранее, такая операция может быть выполнена за $$\lceil log_{2}p\rceil$$ шагов. С учетом количества итераций алгоритма Флойда при использовании модели Хокни общая длительность выполнения коммуникационных операций может быть определена при помощи следующего выражения$$T_p(comm)=n\lceil\log_2p\rceil(\alpha+wn/ \beta),$$ где, как и ранее, $$\alpha$$ – латентность сети передачи данных, $$\beta$$ – пропускная способность сети, а w есть размер элемента матрицы в байтах.

    С учетом полученных соотношений общее время выполнения параллельного алгоритма Флойда может быть определено следующим образом:$$T_p=n^2\cdot\lceil n/p\rceil\cdot\tau+n\cdot\lceil\log_2p\rceil(\alpha+w\cdot n/ \beta),$$ где $$\tau$$ есть время выполнения операции выбора минимального значения.

    10.1.6. Программная реализация

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

    1. Главная функция программы. Реализует логику работы алгоритма, последовательно вызывает необходимые подпрограммы.

    // Программа 10.1. — Алгоритм Флойда
    int ProcRank;    // Ранг текущего процесса
    int ProcNum;     // Количество процессов
    
    // Функция вычисления минимума
    int Min(int A, int B) {
      int Result = (A < B) ? A : B;
    
      if((A < 0)  (B >= 0)) Result = B;
      if((B < 0)  (A >= 0)) Result = A;
      if((A < 0)  (B < 0))  Result = -1;
    
      return Result;
    }
    
    // Главная функция программы
    int main(int argc, char* argv[]) {
      int *pMatrix;     	// Матрица смежности
      int  Size;        	// Размер матрицы смежности
      int *pProcRows;   	// Строки матрицы смежности текущего процесса
      int  RowNum;      	// Число строк для текущего процесса
    
      MPI_Init(argc, argv);
      MPI_Comm_size(MPI_COMM_WORLD, ProcNum);
      MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
    
      // Инициализация данных
      ProcessInitialization(pMatrix, pProcRows, Size, RowNum);
    
      // Распределение данных между процессами
      DataDistribution(pMatrix, pProcRows, Size, RowNum);
    
      // Параллельный алгоритм Флойда
      ParallelFloyd(pProcRows, Size, RowNum);
    
      // Сбор результатов работы алгоритма
      ResultCollection(pMatrix, pProcRows, Size, RowNum);
    
      // Завершение вычислений процесса
      ProcessTermination(pMatrix, pProcRows);
    
      MPI_Finalize();
      return 0;
    }

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

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

    Функция DataDistribution распределяет исходные данные между процессами. Каждый процесс получает горизонтальную полосу матрицы смежности.

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

    Функция ProcessTermination выполняет необходимый вывод результатов решения задачи и освобождает всю ранее выделенную память для хранения данных.

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

    2. Функция ParallelFloyd. Данная функция осуществляет итеративное изменение матрицы смежности в соответствии с алгоритмом Флойда.

    // Параллельный алгоритм Флойда
    void ParallelFloyd(int *pProcRows, int Size, int RowNum) {
      int *pRow = new int[Size];
      int t1, t2;
    
      for(int k = 0; k < Size; k++) {
        // Распределение k-й строки среди процессов 
        RowDistribution(pProcRows, Size, RowNum, k, pRow);
    
        // Обновление элементов матрицы смежности
        for(int i = 0; i < RowNum; i++) 
          for(int j = 0; j < Size; j++)
            if( (pProcRows[i * Size + k] != -1)  (pRow [j]!= -1)){
              t1 = pProcRows[i * Size + j];
              t2 = pProcRows[i * Size + k] + pRow[j];
              pProcRows[i * Size + j] = Min(t1, t2);
            }
      }
        
      delete []pRow;
    }

    3. Функция RowDistribution. Данная функция рассылает k -ю строку матрицы смежности всем процессам программы.

    // Функция для рассылки строки всем процессам
    void RowDistribution(int *pProcRows, int Size, int RowNum, int k,
      int *pRow) {
      int ProcRowRank; 	// Ранг процесса, которому принадлежит k-я строка
      int ProcRowNum;	// Номер k-й строки в полосе матрицы
    
      // Нахождение ранга процесса – владельца k-й строки
      int RestRows = Size;
      int Ind = 0;
      int Num = Size / ProcNum;
      
      for(ProcRowRank = 1; ProcRowRank < ProcNum + 1; ProcRowRank ++)      {
        if(k < Ind + Num ) break;
        RestRows -= Num;
        Ind      += Num;
        Num       = RestRows / (ProcNum - ProcRowRank);
      }
      ProcRowRank = ProcRowRank - 1;
      ProcRowNum  = k - Ind;
    
      if(ProcRowRank == ProcRank)
        // Копирование строки в массив pRow
        copy(pProcRows[ProcRowNum * Size], pProcRows[(ProcRowNum + 1) *
          Size], pRow);
    
      // Распределение k-й строки между процессами  
      MPI_Bcast(pRow, Size, MPI_INT, ProcRowRank, MPI_COMM_WORLD);
    }

    10.1.7. Результаты вычислительных экспериментов

    Эксперименты проводились на вычислительном кластере Нижегородского университета на базе процессоров Intel Xeon 4 EM64T, 3000 МГц и сети Gigabit Ethernet под управлением операционной системы Microsoft Windows Server 2003 Standard x64 Edition и системы управления кластером Microsoft Compute Cluster Server.

    Для оценки длительности $$\tau$$ базовой скалярной операции выбора минимального значения проводилось решение задачи поиска всех кратчайших путей при помощи последовательного алгоритма и полученное таким образом время вычислений делилось на общее количество выполненных операций – в результате для величины $$\tau$$ было получено значение 7,14 нсек. Эксперименты, выполненные для определения параметров сети передачи данных, показали значения латентности $$\alpha$$ и пропускной способности $$\beta$$ соответственно 130 мкс и 53,29 Мбайт/с. Все вычисления производились над числовыми значениями типа int, размер которого на данной платформе равен 4 байта (следовательно, w=4 ).

    Результаты вычислительных экспериментов приведены в таблице 10.1. Эксперименты выполнялись с использованием двух, четырех и восьми процессоров. Время указано в секундах.

    Результаты вычислительных экспериментов для параллельного алгоритма Флойда
    Кол-во вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    Время Ускорение Время Ускорение Время Ускорение
    1000 8,037 4,152 1,936 2,067 3,888 0,941 8,544
    2000 59,812 30,323 1,972 15,375 3,890 8,058 7,423
    3000 197,111 99,264 1,986 50,232 3,924 25,643 7,687
    4000 461,785 232,507 1,986 117,220 3,939 69,946 6,602
    5000 884,622 443,747 1,994 224,441 3,941 128,078 6,907
    (рис 10.4) Графики зависимости ускорения параллельного алгоритма Флойда от числа используемых процессоров при различном количестве вершин графа

    Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретической оценки Tp из (10.3) приведено в таблице 10.2 и на рис. 10.5.

    (рис 10.5) Графики экспериментально установленного времени работы параллельного алгоритма Флойда и теоретической оценки в зависимости от количества вершин графа при использовании двух процессоров
    Сравнение экспериментального и теоретического времени работы алгоритма Флойда
    Количество вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    $$T_1^*$$ $$T_2$$ $$T_2^*$$ $$T_4$$ $$T_4^*$$ $$T_8$$ $$T_8^*$$
    1000 8,038 3,776 4,152 2,196 2,067 1,509 0,941
    2000 59,812 29,123 30,323 15,405 15,375 8,826 8,058
    3000 197,111 97,465 99,264 50,336 50,232 27,307 25,643
    4000 461,785 230,220 232,507 117,701 117,220 62,306 69,946
    5000 884,622 448,811 443,747 228,211 224,441 119,179 128,078

    10.2. Задача нахождения минимального охватывающего дерева

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

    (рис 10.6) Пример взвешенного неориентированного графа (а) и соответствующему ему минимально охватывающего дерева (б)

    Дадим общее описание алгоритма решения поставленной задачи, известного под названием метода Прима ( the Prim method ); более полная информация может быть получена, например, в [].

    10.2.1. Последовательный алгоритм Прима

    Алгоритм начинает работу с произвольной вершины графа, выбираемой в качестве корня дерева, и в ходе последовательно выполняемых итераций расширяет конструируемое дерево до МОД. Пусть VT есть множество вершин, уже включенных алгоритмом в МОД, а величины di, 1<=i<=n, характеризуют дуги минимальной длины от вершин, еще не включенных в дерево, до множества VT, т.е.$$\forall i \notin V_T \Rightarrow d_i = \min\{ w(i,u):u\in V_T,(i,u)\in R\}$$ (если для какой-либо вершины $$i\notin V_{T}$$ не существует ни одной дуги в VT, значение di устанавливается равным $$\infty$$ ). В начале работы алгоритма выбирается корневая вершина МОД s и полагается VT={s}, ds=0.

    Действия, выполняемые на каждой итерации алгоритма Прима, состоят в следующем:

  • определяются значения величин di для всех вершин, еще не включенных в состав МОД;
  • выбирается вершина t графа G, имеющая дугу минимального веса до множества $$V_{T}t:d_{t},\ i\notin V_{T}$$ ;
  • вершина t включается в VT.
  • После выполнения n-1 итерации метода МОД будет сформировано. Вес этого дерева может быть получен при помощи выражения$$W_T = \sum_{i=1}^n d_i.$$

    Трудоемкость нахождения МОД характеризуется квадратичной зависимостью от числа вершин графа T1~n2.

    10.2.2. Разделение вычислений на независимые части

    Оценим возможности параллельного выполнения рассмотренного алгоритма нахождения минимально охватывающего дерева.

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

    Распределение данных между процессорами вычислительной системы должно обеспечивать независимость перечисленных операций алгоритма Прима. В частности, это может быть реализовано, если каждая вершина графа располагается на процессоре вместе со всей связанной с вершиной информацией. Соблюдение данного принципа приводит к тому, что при равномерной загрузке каждый процессор pj, 1<=j<=p, должен содержать:

  • набор вершин$$V_j=\{\nu_{i_j +1}, \nu_{i_j +2}, \ldots, \nu_{i_j +k} \}, \; i_j = k\cdot(j-1), k=\lceil n/p\rceil;$$
  • соответствующий этому набору блок из k величин$$\Delta_j = \{d_{i_j +1},d_{i_j +2},\ldots,d_{i_j +k}\};$$
  • вертикальную полосу матрицы смежности графа G из k соседних столбцов$$A_j=\{\alpha_{i_j +1},\alpha_{i_j +2},\ldots,\alpha_{i_j +k}\} (\alpha_s \text{ есть } s\text{-й столбец матрицы } A);$$
  • общую часть набора Vj и формируемого в процессе вычислений множества вершин VT.
  • Как итог можем заключить, что базовой подзадачей в параллельном алгоритме Прима может служить процедура вычисления блока значений $$\Delta _{j}$$ для вершин Vj матрицы смежности A графа G.

    10.2.3. Выделение информационных зависимостей

    С учетом выбора базовых подзадач общая схема параллельного выполнения алгоритма Прима будет состоять в следующем:

  • определяется вершина t графа G, имеющая дугу минимального веса до множества VT. Для выбора такой вершины необходимо осуществить поиск минимума в наборах величин di, имеющихся на каждом из процессоров, и выполнить сборку полученных значений на одном из процессоров;
  • номер выбранной вершины для включения в охватывающее дерево передается всем процессорам;
  • обновляются наборы величин di с учетом добавления новой вершины.
  • Таким образом, в ходе параллельных вычислений между процессорами выполняются два типа информационных взаимодействий: сбор данных от всех процессоров на одном из процессоров и передача сообщений от одного процессора всем процессорам вычислительной системы.

    10.2.4. Масштабирование и распределение подзадач по процессорам

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

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

    10.2.5. Анализ эффективности параллельных вычислений

    Общий анализ сложности параллельного алгоритма Прима для нахождения минимального охватывающего дерева дает идеальные показатели эффективности параллельных вычислений:$$S_p = \frac{n^2}{(n^2/p)}=p \quad \text{и} \quad E_p = \frac{n^2}{p\cdot(n^2/p)}=1.$$

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

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

    При выполнении вычислений на отдельной итерации параллельного алгоритма Прима каждый процессор определяет номер ближайшей вершины из Vj до охватывающего дерева и осуществляет корректировку расстояний di после расширения МОД. Количество выполняемых операций в каждой из этих вычислительных процедур ограничивается сверху числом вершин, имеющихся на процессорах, т.е. величиной $$\lceil n/p\rceil$$. Как результат, с учетом общего количества итераций n время выполнения вычислительных операций параллельного алгоритма Прима может быть оценено при помощи соотношения:$$T_p(calc)=2n\lceil n/p \rceil \cdot \tau$$ (здесь, как и ранее, $$\tau$$ есть время выполнения одной элементарной скалярной операции).

    Операция сбора данных от всех процессоров на одном из процессоров может быть произведена за $$\lceil log_{2}p\rceil$$ итераций, при этом общая оценка длительности выполнения передачи данных определяется выражением (более подробное рассмотрение данной коммуникационной операции содержится в лекции 3):$$T_p^1(comm)=n(\alpha \log_2 p +3w(p-1)/ \beta),$$ где $$\alpha$$ – латентность сети передачи данных, $$\beta$$ – пропускная способность сети, а w есть размер одного пересылаемого элемента данных в байтах (коэффициент 3 в выражении соответствует числу передаваемых значений между процессорами – длина минимальной дуги и номера двух вершин, которые соединяются этой дугой).

    Коммуникационная операция передачи данных от одного процессора всем процессорам вычислительной системы также может быть выполнена за $$\lceil log_{2}p\rceil$$ итераций при общей оценке времени выполнения вида:$$T_p^2(comm)=n\log_2p(\alpha+w/ \beta)$$

    С учетом всех полученных соотношений общее время выполнения параллельного алгоритма Прима составляет:$$T_p=2n\lceil n/p\rceil\cdot\tau+n(\alpha\cdot\log_2p+3w(p-1)/ \beta+log_2p(\alpha+w/ \beta)).$$

    10.2.6. Результаты вычислительных экспериментов

    Вычислительные эксперименты для оценки эффективности параллельного алгоритма Прима осуществлялись при тех же условиях, что и ранее выполненные (см. п. 10.1.7).

    Для оценки длительности $$\tau$$ базовой скалярной операции проводилось решение задачи нахождения минимального охватывающего дерева при помощи последовательного алгоритма и полученное таким образом время вычислений делилось на общее количество выполненных операций – в результате подобных экспериментов для величины $$\tau$$ было получено значение 4,76 нсек. Все вычисления производились над числовыми значениями типа int, размер которого на данной платформе равен 4 байта (следовательно, w=4 ).

    Результаты вычислительных экспериментов даны в таблице 10.3. Эксперименты проводились с использованием двух, четырех и восьми процессоров. Время указано в секундах.

    Результаты вычислительных экспериментов для параллельного алгоритма Прима
    Кол-во вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    Время Ускорение Время Ускорение Время Ускорение
    1000 0,044 0,248 0,176 0,932 0,047 1,574 0,028
    2000 0,208 0,684 0,304 1,800 0,115 2,159 0,096
    3000 0,485 1,403 0,346 2,214 0,219 3,195 0,152
    4000 0,873 1,946 0,622 3,324 0,263 5,431 0,161
    5000 1,432 2,665 0,736 2,933 0,488 4,119 0,348
    6000 2,189 2,900 0,821 4,291 0,510 7,737 0,283
    7000 3,042 3,236 0,940 6,327 0,481 8,825 0,345
    8000 4,150 4,462 0,930 6,993 0,593 10,390 0,399
    9000 5,622 5,834 0,964 7,475 0,752 10,764 0,522
    10000 7,512 6,990 1,075 8,597 0,874 14,095 0,533
    (рис 10.7) Графики зависимости ускорения параллельного алгоритма Прима от числа используемых процессоров при различном количестве вершин в модели

    Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретической оценки Tp из (10.8) приведено в таблице 10.4 и на рис. 10.8.

    Сравнение экспериментального и теоретического времени работы алгоритма Прима
    Количество вершин Последовательный алгоритм Параллельный алгоритм
    2 процессора 4 процессора 8 процессоров
    $$T_1^*$$ $$T_2$$ $$T_2^*$$ $$T_4$$ $$T_4^*$$ $$T_8$$ $$T_8^*$$
    1000 0,044 0,405 0,248 0,804 0,932 1,205 1,574
    2000 0,208 0,820 0,684 1,613 1,800 2,412 2,159
    3000 0,485 1,245 1,403 2,426 2,214 3,622 3,195
    4000 0,873 1,679 1,946 3,245 3,324 4,834 5,431
    5000 1,432 2,122 2,665 4,068 2,933 6,048 4,119
    6000 2,189 2,575 2,900 4,896 4,291 7,265 7,737
    7000 3,042 3,038 3,236 5,728 6,327 8,484 8,825
    8000 4,150 3,510 4,462 6,566 6,993 9,705 10,390
    9000 5,622 3,991 5,834 7,408 7,475 10,929 10,764
    10000 7,512 4,482 6,990 8,255 8,597 12,155 14,095
    (рис 10.8) Графики экспериментально установленного времени работы параллельного алгоритма Прима и теоретической оценки в зависимости от количества вершин в модели при использовании двух процессоров

    Как можно заметить из табл. 10.4 и рис. 10.8, теоретические оценки определяют время выполнения алгоритма Прима с достаточно высокой погрешностью. Причина такого расхождения может состоять в том, что модель Хокни менее точна при оценке времени передачи сообщений с небольшим объемом передаваемых данных. Для уточнения получаемых оценок необходимым является использование других более точных моделей расчета трудоемкости коммуникационных операций – обсуждение этого вопроса проведено в лекции 3.

    10.3. Задача оптимального разделения графов

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

    (рис 10.9) Пример разделения нерегулярной сети

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

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

    (рис 10.10) Пример графа, моделирующего структуру сети на рис. 10.9

    Дополнительная информация по проблеме разделения графов может быть получена, например, в [].

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

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

    Пусть дан взвешенный неориентированный граф G=(V,E), каждой вершине $$\nu \in V$$ и каждому ребру $$e\in E$$ которого приписан вес. Задача оптимального разделения графа состоит в разбиении его вершин на непересекающиеся подмножества с максимально близкими суммарными весами вершин и минимальным суммарным весом ребер, проходящих между полученными подмножествами вершин.

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

    Далее для простоты изложения учебного материала будем полагать веса вершин и ребер графа равными единице.

    10.3.2. Метод рекурсивного деления пополам

    Для решения задачи разбиения графа можно рекурсивно применить метод бинарного деления ( the binary bisection method ), при котором на первой итерации граф разделяется на две равные части, далее на втором шаге каждая из полученных частей также разбивается на две части и т. д. В данном подходе для разбиения графа на k частей необходимо log2k уровней рекурсии и выполнение k-1 деления пополам. В случае когда требуемое количество разбиений k не является степенью двойки, каждое деление пополам необходимо осуществлять в соответствующем соотношении.

    Поясним схему работы метода деления пополам на примере разделения графа на рис. 10.11 на 5 частей. Сначала граф следует разделить на 2 части в оотношении 2:3 (непрерывная линия), затем правую часть разбиения – в отношении 1:3 (штриховая линия), после этого осталось разделить 2 крайние подобласти слева и справа в отношении 1:1 (штрих-пунктир).

    (рис 10.11) Пример разбиения графа на 5 частей методом рекурсивного деления пополам

    10.3.3. Геометрические методы

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

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

    10.3.3.1. Покоординатное разбиение

    Покоординатное разбиение ( the coordinate nested dissection ) – это метод, основанный на рекурсивном делении пополам сети по наиболее длинной стороне. В качестве иллюстрации на рис. 10.12 показан пример сети, при разделении которой именно такой способ разбиения дает существенно меньшее количество информационных связей между разделенными частями, по сравнению со случаем, когда сеть делится по меньшей (вертикальной) стороне.

    (рис 10.12) Пример разделения сети графическим методом по наибольшей размерности (граница раздела показана жирной линией)

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

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

    10.3.3.2. Рекурсивный инерционный метод деления пополам

    Предыдущая схема могла производить разбиение сети только по линии, перпендикулярной одной из координатных осей. Во многих случаях такое ограничение оказывается критичным для построения качественного разбиения. Достаточно повернуть сеть на рис. 10.12 под острым углом к координатным осям (см. рис. 10.13), чтобы убедиться в этом. Для минимизации границы между подсетями желательна возможность проведения линии разделения с любым требуемым углом поворота. Возможный способ определения угла поворота, используемый в рекурсивном инерционном методе деления пополам ( the recursive inertial bisection ), состоит в использовании главной инерционной оси (см., например, []), считая элементы сети точечными массами. Линия бисекции, ортогональная полученной оси, как правило, дает границу наименьшей длины.

    (рис 10.13) Пример разделения сети методом рекурсивной инерционной бисекции. Стрелкой показана главная инерционная ось

    10.3.3.3. Деление сети с использованием кривых Пеано

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

    Один из таких методов упорядочивает элементы в соответствии с позициями центров их масс вдоль кривых Пеано. Кривые Пеано – это кривые, полностью заполняющие области больших размерностей (например, квадрат или куб). Применение таких кривых обеспечивает близость точек фигуры, которые соответствуют точкам, близким на кривой. После получения списка элементов сети, упорядоченного в зависимости от расположения на кривой, достаточно разделить список на необходимое число частей в соответствии с установленным порядком. Получаемый в результате такого подхода метод носит в литературе наименование алгоритма деления сети с использованием кривых Пеано ( the space-filling curve technique ). Подробнее о методе можно прочитать в работах [, , ].

    (рис 10.14) Пример разделения сети на 3 части с использованием кривых Пеано

    10.3.4. Комбинаторные методы

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

    10.3.4.1. Деление с учетом связности

    С самых общих позиций понятно, что при разделении графа информационная зависимость между разделенными подграфами будет меньше, если соседние вершины (вершины, между которыми имеются дуги) будут находиться в одном подграфе. Алгоритм деления графов с учетом связности ( the levelized nested dissection algorithm ) пытается достичь этого, последовательно добавляя к формируемому подграфу соседей. На каждой итерации алгоритма происходит разделение графа на 2 части. Таким образом, разделение графа на требуемое число частей достигается путем рекурсивного применения алгоритма.

    Общая схема алгоритма может быть описана при помощи следующего набора правил.

    Алгоритм 10.2. Общая схема выполнения алгоритма деления графов с учетом связности

  • Iteration = 0.
  • Присвоение номера Iteration произвольной вершине графа.
  • Присвоение ненумерованным соседям вершин с номером Iteration номера Iteration + 1.
  • Iteration = Iteration + 1.
  • Если еще есть неперенумерованные соседи, то переход на шаг 3.
  • Разделение графа на 2 части в порядке нумерации.
  • Для минимизации информационных зависимостей имеет смысл в качестве начальной выбирать граничную вершину. Поиск такой вершины можно осуществить методом, близким к рассмотренной схеме. Так, перенумеровав вершины графа в соответствии с алгоритмом 10.2 (начиная нумерацию из произвольной вершины), мы можем взять любую вершину с максимальным номером. Как нетрудно убедиться, она будет граничной.

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

    (рис 10.15) Пример работы алгоритма деления графов с учетом связности

    10.3.4.2. Алгоритм Кернигана – Лина

    В алгоритме Кернигана – Лина ( the Kernighan – Lin algorithm ) используется несколько иной подход для решения проблемы оптимального разбиения графа – предполагается, что некоторое начальное разбиение графа уже существует, затем имеющееся приближение улучшается в течение некоторого количества итераций. Применяемый способ улучшения в алгоритме Кернигана – Лина состоит в обмене вершинами между подмножествами имеющегося разбиения графа (см. рис. 10.16). Для формирования требуемого количества частей графа может быть использована, как и ранее, рекурсивная процедура деления пополам.

    Общая схема одной итерации алгоритма Кернигана – Лина может быть представлена следующим образом.

    Алгоритм 10.3. Общая схема алгоритма Кернигана – Лина

  • Формирование множества пар вершин для перестановки. Из вершин, которые еще не были переставлены на данной итерации, формируются все возможные пары (в парах должно присутствовать по одной вершине из каждой части имеющегося разбиения графа ).
  • Построение новых вариантов разбиения графа. Каждая пара, подготовленная на шаге 1, поочередно используется для обмена вершин между частями имеющегося разбиения графа для получения множества новых вариантов деления.
  • Выбор лучшего варианта разбиения графа. Для сформированного на шаге 2 множества новых делений графа выбирается лучший вариант. Этот вариант далее фиксируется как новое текущее разбиение графа, а соответствующая выбранному варианту пара вершин отмечается как использованная на текущей итерации алгоритма.
  • Проверка использования всех вершин. При наличии в графе вершин, еще не использованных при перестановках, выполнение итерации алгоритма снова продолжается с шага 1. Если же перебор вершин графа завершен, далее следует шаг 5.
  • Выбор наилучшего варианта разбиения графа. Среди всех разбиений графа, полученных на шаге 3 проведенных итераций, выбирается (и фиксируется) наилучший вариант разбиения графа.
  • Поясним дополнительно, что на шаге 2 итерации алгоритма перестановка вершин каждой очередной пары осуществляется для одного и того же разбиения графа, выбранного до начала выполнения итерации или определенного на шаге 3. Общее количество выполняемых итераций, как правило, фиксируется заранее и является параметром алгоритма (за исключением случая остановки при отсутствии улучшения разбиения на очередной итерации).

    (рис 10.16) Пример перестановки двух вершин (выделены серым) в методе Кернигана – Лина

    10.3.5. Сравнение алгоритмов разбиения графов

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

    Сравнительная таблица некоторых алгоритмов разделения графов
    Алгоритмы Необходимость координатной информации Точность Время выполнения Возможности для распараллеливания
    Покоординатное разбиение Да $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Рекурсивный инерционный метод деления пополам Да $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Деление с учетом связности Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    Алгоритм Кернигана - Лина 1 итерация Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    10 итераций Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$
    50 итераций Нет $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$ $$ullet$$

    Столбец "Необходимость координатной информации" отмечает использование алгоритмом координатной информации об элементах сети или вершинах графа.

    Столбец "Точность" дает качественную характеристику величины приближения получаемых алгоритмом решений к оптимальным вариантам разбиения графов. Каждый дополнительный закрашенный кружок определяет примерно 10%-процентное улучшение точности приближения.

    Столбец "Время выполнения" показывает относительное время, затрачиваемое различными алгоритмами разбиения. Каждый дополнительный закрашенный кружок соответствует увеличению времени разбиения примерно в 10 раз.

    Столбец "Возможности для распараллеливания" характеризует свойства алгоритмов для параллельного выполнения. Алгоритм Кернигана – Лина при выполнении только одной итерации почти не поддается распараллеливанию. Этот же алгоритм при большем количестве итераций, а также метод деления с учетом связности могут быть распараллелены со средней эффективностью. Алгоритм покоординатного разбиения и рекурсивный инерционный метод деления пополам обладают высокими показателями для распараллеливания.

    10.4. Краткий обзор лекции

    В лекции рассмотрен ряд алгоритмов для решения типовых задач обработки графов. Кроме того, приведен обзор методов разделения графа.

    В подразделе 10.1 представлен алгоритм Флойда ( the Floyd algorithm ) – дается общая вычислительная схема последовательного варианта метода, обсуждаются способы его распараллеливания, проводится анализ эффективности получаемых параллельных вычислений, рассматривается программная реализация метода и приводятся результаты вычислительных экспериментов. Используемый подход к распараллеливанию алгоритма Флойда состоит в разделении вершин графа между процессорами, а необходимое при этом информационное взаимодействие состоит в передаче одной строки матрицы смежности от одного процессора всем процессорам вычислительной системы на каждой итерации метода.

    В подразделе 10.2 рассматривается алгоритм Прима ( the Prim algorithm ) для решения задачи поиска минимального охватывающего дерева (остова) неориентированного взвешенного графа. Остовом графа называют связный подграф без циклов (дерево), который содержит все вершины исходного графа и ребра, имеющие минимальный суммарный вес. Для алгоритма дается общее описание его исходного последовательного варианта, определяются возможные способы его параллельного выполнения, теоретические оценки ускорения и эффективности параллельных вычислений ; также рассматриваются результаты проведенных вычислительных экспериментов. Параллельный вариант алгоритма Прима, как и в предыдущем случае, основывается на разделении вершин графа между процессорами при несколько большем объеме информационных взаимодействий – на каждой итерации алгоритма необходимой является операция сбора данных на одном процессоре и последующая рассылка номера выбранной вершины графа всем процессорам вычислительной системы.

    Рассматриваемая в подразделе 10.3 задача оптимального разделения графов является важной для многих научных исследований, использующих параллельные вычисления. Для примера в подразделе приведен общий способ перехода от двумерной или трехмерной сети, моделирующей процесс вычислений, к соответствующему ей графу. Для решения задачи разбиения графов были рассмотрены геометрические методы, использующие при разделении сетей только координатную информацию об узлах сети, и комбинаторные алгоритмы, руководствующиеся смежностью вершин графа. К числу рассмотренных геометрических методов относятся покоординатное разбиение ( the coordinate nested dissection method ), рекурсивный инерционный метод деления пополам ( the recursive inertial bisection method ), деление сети с использованием кривых Пеано ( the space-filling curve techniques ). К числу рассмотренных комбинаторных алгоритмов относятся деление с учетом связности ( the levelized nested dissection ) и алгоритм Кернигана – Лина ( the Kernighan – Lin algorithm ). Для сопоставления рассмотренных подходов приводится общая сравнительная характеристика алгоритмов по времени выполнения, точности получаемого решения, возможностям для распараллеливания и т.п.

    10.5. Обзор литературы

    Дополнительная информация по алгоритмам Флойда и Прима может быть получена, например, в [].

    Подробное рассмотрение вопросов, связанных с проблемой разделения графов, содержится в работах [, , , , , , , , , ].

    Параллельные алгоритмы разделения графов рассматриваются в [, , , , , , ].

    10.6. Контрольные вопросы

  • Приведите определение графа. Какие основные способы используются для задания графов?
  • В чем состоит задача поиска всех кратчайших путей?
  • Приведите общую схему алгоритма Флойда. Какова трудоемкость алгоритма?
  • В чем состоит способ распараллеливания алгоритма Флойда?
  • В чем заключается задача нахождения минимального охватывающего дерева? Приведите пример использования задачи на практике.
  • Приведите общую схему алгоритма Прима. Какова трудоемкость алгоритма?
  • В чем состоит способ распараллеливания алгоритма Прима?
  • В чем отличие геометрических и комбинаторных методов разделения графа? Какие методы являются более предпочтительными? Почему?
  • Приведите описание метода покоординатного разбиения и алгоритма разделения с учетом связности. Какой из этих методов является более простым для реализации?
  • 10.7. Задачи и упражнения

  • Используя приведенный программный код, выполните реализацию параллельного алгоритма Флойда. Проведите вычислительные эксперименты. Постройте теоретические оценки с учетом параметров используемой вычислительной системы. Сравните полученные оценки с экспериментальными данными.
  • Выполните реализацию параллельного алгоритма Прима. Проведите вычислительные эксперименты. Постройте теоретические оценки с учетом параметров используемой вычислительной системы. Сравните полученные оценки с экспериментальными данными.
  • Разработайте программную реализацию алгоритма Кернигана – Лина. Дайте оценку возможности распараллеливания этого алгоритма.
  • Вернуться к учебному плану