Параллельные алгоритмы для минимальных остовных деревьев - Parallel algorithms for minimum spanning trees
В теории графов минимальное покрывающее дерево (MST) из графа с и представляет собой дерево подграф из который содержит все его вершины и имеет минимальный вес.
MST - это полезные и универсальные инструменты, используемые в самых разных практических и теоретических областях. Например, компания, которая хочет поставлять в несколько магазинов определенный продукт с одного склада, может использовать MST, исходящий на складе, для расчета кратчайших путей к каждому магазину компании. В этом случае магазины и склад представлены в виде вершин, а дороги между ними - в виде ребер. На каждом ребре обозначена длина соответствующего дорожного соединения.
If не взвешен по ребрам, каждое остовное дерево имеет одинаковое количество ребер и, следовательно, одинаковый вес. В случае взвешенного по ребрам остовное дерево, сумма весов ребер которого является наименьшей среди всех остовных деревьев , называется минимальным остовным деревом (MST). Это не обязательно уникально. В более общем смысле, графы, которые не обязательно связаны, имеют минимальные остовные леса , которые состоят из объединения MST для каждого связного компонента .
Поскольку поиск MST является широко распространенной проблемой в теории графов, существует множество последовательных алгоритмов для ее решения. Среди них алгоритмы Прима , Крускала и Борувки , каждый из которых использует разные свойства MST. Все они работают одинаково - подмножество итеративно растет до тех пор, пока не будет обнаружен действительный MST. Однако, поскольку практические проблемы часто бывают довольно большими (дорожные сети иногда имеют миллиарды ребер), производительность является ключевым фактором. Один из вариантов его улучшения - распараллеливание известных алгоритмов MST .
Алгоритм Прима
Этот алгоритм использует свойство сокращения MST. Ниже представлена простая реализация высокоуровневого псевдокода:
where is a random vertex in repeat times find lightest edge s.t. but return T
Каждое ребро наблюдается ровно дважды, а именно при проверке каждой из его конечных точек. Каждая вершина проверяется ровно один раз для общего количества операций, не считая выбора самого светлого ребра на каждой итерации цикла. Этот выбор часто выполняется с использованием очереди приоритетов (PQ). Для каждого ребра при более одной операции decreaseKey ( амортизируется в ) выполняется , и каждый цикл итерации выполняет одну операцию deleteMin ( ). Таким образом , с помощью чисел Фибоначчи осыпает общее время выполнения алгоритма Прима является асимптотически в .
Важно отметить, что цикл по своей сути является последовательным и не может быть должным образом распараллелен. Это так, поскольку самая светлая кромка с одной входящей и включенной конечной точкой может измениться при добавлении ребер в . Таким образом, невозможно выполнить два выбора наиболее светлого края одновременно. Однако попытки распараллеливания все же есть .
Одна из возможных идей - использовать процессоры для поддержки доступа PQ на машине EREW-PRAM , тем самым снижая общее время выполнения до .
Алгоритм Крускала
Алгоритм MST Крускала использует свойство цикла MST. Ниже представлено высокоуровневое представление псевдокода.
forest with every vertex in its own subtree foreach in ascending order of weight if and in different subtrees of return T
Поддеревья хранятся в структурах данных union-find , поэтому проверка того, находятся ли две вершины в одном поддереве, возможна в амортизированной, где - обратная функция Аккермана . Таким образом, общее время выполнения алгоритма составляет . Здесь обозначает однозначную обратную функцию Аккермана, для которой любой реалистичный ввод дает целое число меньше пяти.
Подход 1: Распараллеливание этапа сортировки
Как и в алгоритме Прима, в подходе Крускала есть компоненты, которые нельзя распараллелить в его классическом варианте. Например, определение того, находятся ли две вершины в одном поддереве, трудно распараллелить, поскольку две операции объединения могут попытаться объединить одни и те же поддеревья одновременно. На самом деле единственная возможность распараллеливания - это этап сортировки. Поскольку сортировка в оптимальном случае на процессорах линейна , общее время работы может быть уменьшено до .
Подход 2: Фильтр-Краскал
Другой подход - изменить исходный алгоритм, увеличив его более агрессивно. Эта идея была представлена Осиповым и соавт. Основная идея Filter-Kruskal состоит в том, чтобы разделить ребра аналогично быстрой сортировке и отфильтровать ребра, которые соединяют вершины, принадлежащие одному дереву, чтобы снизить стоимость сортировки. Ниже представлено высокоуровневое представление псевдокода.
filterKruskal(): if KruskalThreshold: return kruskal() pivot = chooseRandom() , partition(, pivot) filterKruskal() filter() filterKruskal() return partition(, pivot): foreach : if weight() pivot: else return (, ) filter(): foreach : if find-set(u) find-set(v): return
Filter-Kruskal лучше подходит для распараллеливания, поскольку сортировка, разбиение и фильтрация имеют интуитивно простое распараллеливание, когда границы просто разделяются между ядрами.
Алгоритм Борувки
Основная идея алгоритма Борувки - сжатие ребер . Ребро сжимается, сначала удаляя его из графа, а затем перенаправляя каждое ребро на . Эти новые кромки сохраняют свой прежний вес. Если цель состоит не только в определении веса MST, но и в том, какие ребра он включает, необходимо отметить, между какими парами вершин было сжато ребро. Представление псевдокода высокого уровня представлено ниже.
while for lightest for contract return T
Возможно, что стягивания приводят к множеству ребер между парой вершин. Интуитивно понятный способ выбора самых легких из них невозможен в . Однако, если все сокращения, имеющие общую вершину, выполняются параллельно, это выполнимо. Рекурсия останавливается, когда остается только одна вершина, что означает, что алгоритму требуется не больше итераций, что приводит к общему времени выполнения в .
Распараллеливание
Одно возможное распараллеливание этого алгоритма дает полилогарифмическую временную сложность, т. Е. Существует константа, так что . Здесь обозначает время выполнения графа с ребрами, вершинами на машине с процессорами. Основная идея заключается в следующем:
while find lightest incident edges // assign the corresponding subgraph to each vertex // contract each subgraph //
Затем MST состоит из всех найденных самых светлых ребер.
Это распараллеливание использует представление графа массива смежности для . Он состоит из трех массивов - длины для вершин, длины для концов каждого из ребер и длины для веса ребер. Теперь для вершины другой конец каждого инцидентного ребра можно найти в записях между и . Вес -го ребра можно найти в . Тогда -я ребро в находится между вершинами и тогда и только тогда, когда и .
Поиск самого легкого края инцидента
Сначала ребра распределяются между каждым из процессоров. -М процессор принимает края , сохраненные между и . Кроме того, каждому процессору необходимо знать, к какой вершине принадлежат эти ребра (поскольку хранится только одна из конечных точек ребра), и сохранять это в массиве . Получение этой информации возможно при использовании бинарного поиска или при использовании линейного поиска. На практике последний подход иногда оказывается быстрее, хотя асимптотически он хуже.
Теперь каждый процессор определяет самое светлое ребро, инцидентное каждой из его вершин.
find(, ) for if if
Здесь возникает проблема: некоторые вершины обрабатываются более чем одним процессором. Возможное решение этой проблемы состоит в том, что каждый процессор имеет свой собственный массив, который позже объединяется с массивами других с помощью сокращения. Каждый процессор имеет не более двух вершин, которые также обрабатываются другими процессорами, и каждое сокращение находится в пределах . Таким образом, общее время выполнения этого шага составляет .
Назначение подграфов вершинам
Обратите внимание на граф, состоящий исключительно из ребер, собранных на предыдущем шаге. Эти ребра направлены от вершины, к которой они наиболее легкое инцидентное ребро. Полученный граф распадается на несколько слабосвязных компонент. Цель этого шага - присвоить каждой вершине компонент, частью которого она является. Обратите внимание, что каждая вершина имеет ровно одно исходящее ребро, и поэтому каждый компонент является псевдодеревом - деревом с единственным дополнительным ребром, которое проходит параллельно самому светлому ребру в компоненте, но в противоположном направлении. Следующий код преобразует это дополнительное ребро в цикл:
parallel forAll if
Теперь каждый компонент слабой связности представляет собой ориентированное дерево, у корня которого есть петля . Этот корень выбран как представитель каждого компонента. Следующий код использует удвоение для присвоения каждой вершине своего представителя:
while forAll
Теперь каждый подграф - это звезда . С некоторыми продвинутыми техниками этот шаг требует времени.
Сужение подграфов
На этом шаге каждый подграф сжимается до одной вершины.
number of subgraphs find a bijective function star root
Найти биективную функцию можно с помощью префиксной суммы. Как мы теперь имеем новый набор вершин и ребра массив смежности должен быть восстановлен, что можно сделать с помощью Integersort на в время.
Сложность
Каждая итерация теперь требует времени, и, как и в последовательном случае, есть итерации, в результате чего общее время выполнения составляет . Если эффективность алгоритма находится в пределах и он относительно эффективен. Если тогда абсолютно работоспособен.
Дальнейшие алгоритмы
Существует несколько других параллельных алгоритмов, которые решают проблему поиска MST. При линейном количестве процессоров этого можно достичь за . Бадер и Конг представили MST-алгоритм, который был в пять раз быстрее на восьми ядрах, чем оптимальный последовательный алгоритм.
Еще одна проблема - это модель внешней памяти - есть предложенный алгоритм Дементьева и др. который, как утверждается, всего в два-пять раз медленнее, чем алгоритм, который использует только внутреннюю память