Поиск в ширину (BFS): различия между версиями
| [непроверенная версия] | [досмотренная версия] |
Elijah (обсуждение | вклад) |
ASA (обсуждение | вклад) |
||
| (не показано 16 промежуточных версий 4 участников) | |||
| Строка 1: | Строка 1: | ||
{{algorithm | {{algorithm | ||
| name = Алгоритм поиска в ширину (BFS) | | name = Алгоритм поиска в ширину (BFS) | ||
| serial_complexity = <math>O( | | serial_complexity = <math>O(|V| + |E|)</math> | ||
| pf_height = <math>N/A, \max O( | | pf_height = <math>N/A, \max O(|V|) </math> | ||
| pf_width = <math>N/A, \max O( | | pf_width = <math>N/A, \max O(|E|) </math> | ||
| input_data = <math>O( | | input_data = <math>O(|V| + |E|)</math> | ||
| output_data = <math>O( | | output_data = <math>O(|V|)</math> | ||
}} | }} | ||
| Строка 18: | Строка 18: | ||
Алгоритм основан на обходе вершин графа "по слоям". На каждом шаге есть множество "передовых" вершин, для смежных к которым производится проверка, относятся ли они к еще не посещенным. Все еще не посещенные вершины добавляются в новое множество "передовых" вершин, обрабатываемых на следующем шаге. Изначально в множество "передовых" вершин входит только вершина-источник, от которой и начинается обход. | Алгоритм основан на обходе вершин графа "по слоям". На каждом шаге есть множество "передовых" вершин, для смежных к которым производится проверка, относятся ли они к еще не посещенным. Все еще не посещенные вершины добавляются в новое множество "передовых" вершин, обрабатываемых на следующем шаге. Изначально в множество "передовых" вершин входит только вершина-источник, от которой и начинается обход. | ||
В последовательном случае алгоритм имеет алгоритмическую сложность <math>O( | В последовательном случае алгоритм имеет алгоритмическую сложность <math>O(|V| + |E|)</math>, где <math>|V|</math> - число вершин в графе, <math>|E|</math> - число ребер в графе. | ||
=== Математическое описание алгоритма === | === Математическое описание алгоритма === | ||
| Строка 24: | Строка 24: | ||
Пусть задан граф <math>G = (V, E)</math> без весов, и с выделенной вершиной-источником <math>u</math>. Путем <math>P(u,v)</math> между вершинами <math>u</math> и <math>v</math> называется множество ребер <math>(u, v_1), (v_1, v_2), ... (v_{n-1}, v)</math>. Длиной пути <math>d(u,v)</math> обозначим число ребер в данном пути между вершинами <math>u</math> и <math>v</math>. Поиск в ширину находит кратчайшие пути <math>d(u,v)</math> от вершины <math>u</math> до всех остальных вершин графа описанным далее образом. | Пусть задан граф <math>G = (V, E)</math> без весов, и с выделенной вершиной-источником <math>u</math>. Путем <math>P(u,v)</math> между вершинами <math>u</math> и <math>v</math> называется множество ребер <math>(u, v_1), (v_1, v_2), ... (v_{n-1}, v)</math>. Длиной пути <math>d(u,v)</math> обозначим число ребер в данном пути между вершинами <math>u</math> и <math>v</math>. Поиск в ширину находит кратчайшие пути <math>d(u,v)</math> от вершины <math>u</math> до всех остальных вершин графа описанным далее образом. | ||
В начале работы алгоритма расстояние до вершины-источника <math>d(u)=0</math>, до остальных вершин <math>d(v) = \infty, \forall v \neq u </math>. | В начале работы алгоритма расстояние до вершины-источника <math>d(u)=0</math>, до остальных вершин <math>d(v) = \infty, \forall v \neq u </math>. Также в начале работы алгоритма инициализируется множество <math>F = \{u\}</math>. | ||
Далее на каждом шаге | Далее на каждом шаге алгоритма строится множество вершин <math>P = {w} </math>, таких, что для <math>\forall v \in F \exists (v, w) \in E | d(w) = \infty </math>, при этом обновляются расстояния <math>d(w)=d(v)+1</math> для <math>\forall w \in P </math>. Затем производится переход на следующий шаг до тех пор, пока <math>P \neq \emptyset</math>; при этом в начале каждого шага множество F заменяется на P. | ||
=== Вычислительное ядро алгоритма === | === Вычислительное ядро алгоритма === | ||
Вычислительным ядром алгоритма является обход вершин, | Вычислительным ядром алгоритма является обход вершин, смежных с выбранной вершиной <math>v</math>, с последующим добавлением еще не посещенных вершин в множество <math>P</math>. Данная операция выполняется на каждом шаге для каждой вершины <math>v \in F</math>. | ||
=== Макроструктура алгоритма === | === Макроструктура алгоритма === | ||
| Строка 89: | Строка 89: | ||
=== Последовательная сложность алгоритма === | === Последовательная сложность алгоритма === | ||
Алгоритм имеет последовательную сложность <math>O( | Алгоритм имеет последовательную сложность <math>O(|V| + |E|)</math>, где <math>|V|</math> и <math>|E|</math> - число вершин и ребер графа соответственно: алгоритм инициализирует начальный массив расстояний - <math>O(|V|)</math> операций, а затем обходит каждую вершину один единственный раз - <math>O(|E|)</math> операций. Данная оценка верна в случае, если формат хранения графа позволяет обходить вершины, смежные к выбранной (к примеру форматы списка смежности, сжатого списка смежности). При использовании других форматов оценка сложности может быть большей. | ||
=== Информационный граф === | === Информационный граф === | ||
| Строка 107: | Строка 107: | ||
[4] - добавление еще не посещенных вершин в множество <math>P</math>. | [4] - добавление еще не посещенных вершин в множество <math>P</math>. | ||
[5] - замена множества <math>F</math> на <math>P</math> и проверка его пустоты. В случае | [5] - замена множества <math>F</math> на <math>P</math> и проверка его пустоты. В случае непустого множества - переход на следующую итерацию, иначе завершение работы алгоритма. | ||
Данный алгоритм имеет один важный недостаток при реализации: операция [4] требует бесконфликтной возможности добавления элементов в множество P, что, на практике, всегда будет сводиться к сериализации обращений к структуре данных, | Данный алгоритм имеет один важный недостаток при реализации: операция [4] требует бесконфликтной возможности добавления элементов в множество P, что, на практике, всегда будет сводиться к сериализации обращений к структуре данных, моделирующей данное множество. | ||
В результате часто используется модификация алгоритма (далее алгоритм-2), использующая набор независимых структур данных для каждого из параллельных процессов. Информационный граф данного подхода приведен на рисунке 2. | В результате часто используется модификация алгоритма (далее алгоритм-2), использующая набор независимых структур данных для каждого из параллельных процессов. Информационный граф данного подхода приведен на рисунке 2. | ||
| Строка 129: | Строка 129: | ||
[6] - проверка условия выхода из цикла | [6] - проверка условия выхода из цикла | ||
Кроме того, в случае, если реализация структур данных, моделирующих множества <math>F</math> и <math>P</math> невозможна, может использоваться квадратичный по сложности алгоритм, схожий с алгоритм Беллмана-Форда. Основная идея заключается в том, что на каждом шаге производится обход всех ребер графа с обновлением текущего массива дистанций. Информационный граф данной модификации алгоритма приведен на рисунке 3. | Кроме того, в случае, если реализация структур данных, моделирующих множества <math>F</math> и <math>P</math>, невозможна, может использоваться квадратичный по сложности алгоритм, схожий с алгоритм Беллмана-Форда. Основная идея заключается в том, что на каждом шаге производится обход всех ребер графа с обновлением текущего массива дистанций. Информационный граф данной модификации алгоритма приведен на рисунке 3. | ||
[[Файл:quadratic_BFS_ig.png|thumb|center|500px|Рисунок 3. Информационный граф алгоритма BFS (квадратичный подход к распараллеливанию).]] | [[Файл:quadratic_BFS_ig.png|thumb|center|500px|Рисунок 3. Информационный граф алгоритма BFS (квадратичный подход к распараллеливанию).]] | ||
| Строка 137: | Строка 137: | ||
[1] - инициализация расстояний до вершины-источника | [1] - инициализация расстояний до вершины-источника | ||
[2] - | [2] - инициализация расстояний до остальных вершин графа | ||
[3] - загрузка информации об очередном ребре и | [3] - загрузка информации об очередном ребре и обновление дистанций до соответствующих вершин. | ||
[4] - проверка | [4] - проверка условия выхода из цикла | ||
=== Ресурс параллелизма алгоритма === | === Ресурс параллелизма алгоритма === | ||
| Строка 147: | Строка 147: | ||
В ходе работы классический вариант алгоритма обходит граф по слоям. В каждый слой добавляются еще не посещенные вершины, достижимые из вершин предыдущего слоя. Обход вершин каждого слоя, как и их соседей, может производиться параллельно. Точно оценить число вершин в каждом слое невозможно в силу того, что их количество зависит от структуры связанности входного графа. Аналогично невозможно оценить число шагов алгоритма, за которое будут найдены все кратчайшие пути. | В ходе работы классический вариант алгоритма обходит граф по слоям. В каждый слой добавляются еще не посещенные вершины, достижимые из вершин предыдущего слоя. Обход вершин каждого слоя, как и их соседей, может производиться параллельно. Точно оценить число вершин в каждом слое невозможно в силу того, что их количество зависит от структуры связанности входного графа. Аналогично невозможно оценить число шагов алгоритма, за которое будут найдены все кратчайшие пути. | ||
Произведем оценку ширины ярусно-параллельной формы алгоритма через максимальное число вершин p в слое среди всех шагов алгоритма. Тогда число параллельных операций на данном слое будет равно сумме числа смежных вершин для каждой вершины слоя: <math>\sum_{n=1}^{p} degree(v_i)</math>, при | Произведем оценку ширины ярусно-параллельной формы алгоритма через максимальное число вершин p в слое среди всех шагов алгоритма. Тогда число параллельных операций на данном слое будет равно сумме числа смежных вершин для каждой вершины слоя: <math>\sum_{n=1}^{p} degree(v_i)</math>, при этом для каждого слоя данное значение будет различным. Высота ярусно-параллельной формы будет равна числу шагов в алгоритме и может быть оценена только сверху (не более <math>|V|</math>). | ||
При квадратичном подходе к параллельной реализации алгоритма на каждом шаге | При квадратичном подходе к параллельной реализации алгоритма на каждом шаге выполняется <math>O(|E|)</math> операций, которые могут быть выполнены параллельно; таким образом, ширина ЯПФ данной модификации алгоритма равна <math>O(|E|)</math>. Число шагов алгоритма, как и в классическом случае, зависит от структуры графа и может быть оценено сверху как <math>O(|V|)</math>. | ||
=== Входные и выходные данные алгоритма === | === Входные и выходные данные алгоритма === | ||
'''Входные данные''': граф <math>G(V, E)</math> | '''Входные данные''': граф <math>G(V, E)</math>, <math>|V|</math> вершин <math>v_i</math> и <math>|E|</math> рёбер <math>e_j = (v^{(1)}_{j}, v^{(2)}_{j})</math>, вершина-источник <math>u</math>. | ||
'''Объём входных данных''': <math>O( | '''Объём входных данных''': <math>O(|V| + |E|)</math>. | ||
'''Выходные данные''' (возможные варианты): | '''Выходные данные''' (возможные варианты): | ||
| Строка 160: | Строка 160: | ||
# для каждой вершины <math>v</math> исходного графа значение достижимости (достижима или нет) от вершины-источника <math>u</math>. | # для каждой вершины <math>v</math> исходного графа значение достижимости (достижима или нет) от вершины-источника <math>u</math>. | ||
'''Объём выходных данных''': <math>O( | '''Объём выходных данных''': <math>O(|V|)</math>. | ||
=== Свойства алгоритма === | === Свойства алгоритма === | ||
| Строка 166: | Строка 166: | ||
== Программная реализация алгоритма == | == Программная реализация алгоритма == | ||
=== Особенности реализации последовательного алгоритма === | === Особенности реализации последовательного алгоритма === | ||
=== Возможные способы и особенности параллельной реализации алгоритма === | === Возможные способы и особенности параллельной реализации алгоритма === | ||
=== | === Результаты прогонов === | ||
=== Выводы для классов архитектур === | === Выводы для классов архитектур === | ||
== Литература == | == Литература == | ||
| Строка 189: | Строка 174: | ||
<references /> | <references /> | ||
[[Категория: | [[Категория:Статьи в работе]] | ||
[[En:Breadth-first search (BFS)]] | |||
Текущая версия от 07:59, 1 июля 2022
| Алгоритм поиска в ширину (BFS) | |
| Последовательный алгоритм | |
| Последовательная сложность | [math]\displaystyle{ O(|V| + |E|) }[/math] |
| Объём входных данных | [math]\displaystyle{ O(|V| + |E|) }[/math] |
| Объём выходных данных | [math]\displaystyle{ O(|V|) }[/math] |
| Параллельный алгоритм | |
| Высота ярусно-параллельной формы | [math]\displaystyle{ N/A, \max O(|V|) }[/math] |
| Ширина ярусно-параллельной формы | [math]\displaystyle{ N/A, \max O(|E|) }[/math] |
Основные авторы описания: И.В.Афанасьев
Свойства и структура алгоритма
Общее описание алгоритма
Поиск в ширину (англ. Breadth-First Search, BFS) позволяет вычислить кратчайшие расстояния (в терминах количества рёбер) от выделенной вершины ориентированного графа до всех остальных вершин, и/или построить корневое направленное дерево, расстояния в котором совпадают с расстояниями в исходном графе. Кроме того, поиск в ширину позволяет решать задачу проверки достижимости (существуют ли пути между вершиной источником и остальными вершинами графа). Впервые алгоритм поиска в ширину описан в работах Мура[1] и Ли[2].
Алгоритм основан на обходе вершин графа "по слоям". На каждом шаге есть множество "передовых" вершин, для смежных к которым производится проверка, относятся ли они к еще не посещенным. Все еще не посещенные вершины добавляются в новое множество "передовых" вершин, обрабатываемых на следующем шаге. Изначально в множество "передовых" вершин входит только вершина-источник, от которой и начинается обход.
В последовательном случае алгоритм имеет алгоритмическую сложность [math]\displaystyle{ O(|V| + |E|) }[/math], где [math]\displaystyle{ |V| }[/math] - число вершин в графе, [math]\displaystyle{ |E| }[/math] - число ребер в графе.
Математическое описание алгоритма
Пусть задан граф [math]\displaystyle{ G = (V, E) }[/math] без весов, и с выделенной вершиной-источником [math]\displaystyle{ u }[/math]. Путем [math]\displaystyle{ P(u,v) }[/math] между вершинами [math]\displaystyle{ u }[/math] и [math]\displaystyle{ v }[/math] называется множество ребер [math]\displaystyle{ (u, v_1), (v_1, v_2), ... (v_{n-1}, v) }[/math]. Длиной пути [math]\displaystyle{ d(u,v) }[/math] обозначим число ребер в данном пути между вершинами [math]\displaystyle{ u }[/math] и [math]\displaystyle{ v }[/math]. Поиск в ширину находит кратчайшие пути [math]\displaystyle{ d(u,v) }[/math] от вершины [math]\displaystyle{ u }[/math] до всех остальных вершин графа описанным далее образом.
В начале работы алгоритма расстояние до вершины-источника [math]\displaystyle{ d(u)=0 }[/math], до остальных вершин [math]\displaystyle{ d(v) = \infty, \forall v \neq u }[/math]. Также в начале работы алгоритма инициализируется множество [math]\displaystyle{ F = \{u\} }[/math].
Далее на каждом шаге алгоритма строится множество вершин [math]\displaystyle{ P = {w} }[/math], таких, что для [math]\displaystyle{ \forall v \in F \exists (v, w) \in E | d(w) = \infty }[/math], при этом обновляются расстояния [math]\displaystyle{ d(w)=d(v)+1 }[/math] для [math]\displaystyle{ \forall w \in P }[/math]. Затем производится переход на следующий шаг до тех пор, пока [math]\displaystyle{ P \neq \emptyset }[/math]; при этом в начале каждого шага множество F заменяется на P.
Вычислительное ядро алгоритма
Вычислительным ядром алгоритма является обход вершин, смежных с выбранной вершиной [math]\displaystyle{ v }[/math], с последующим добавлением еще не посещенных вершин в множество [math]\displaystyle{ P }[/math]. Данная операция выполняется на каждом шаге для каждой вершины [math]\displaystyle{ v \in F }[/math].
Макроструктура алгоритма
Алгоритм последовательно уточняет значения функции [math]\displaystyle{ d(v) }[/math].
Структуру можно описать следующим образом:
1. Инициализация: всем вершинам присваивается предполагаемое расстояние [math]\displaystyle{ d(v)=\infty }[/math], кроме вершины-источника, для которой [math]\displaystyle{ d(u)=0 }[/math] .
2. Помещение вершины источника [math]\displaystyle{ v }[/math] в множество "передовых" вершин [math]\displaystyle{ F }[/math].
3. Обход вершин множества [math]\displaystyle{ F }[/math].
а) Инициализация множества [math]\displaystyle{ P=\emptyset }[/math].
б) Для каждой вершины [math]\displaystyle{ v \in F }[/math] обход всех вершин [math]\displaystyle{ w | \exists (v, w) }[/math] (смежных с ней), c помещением в множество [math]\displaystyle{ P }[/math] таких вершин [math]\displaystyle{ w | d(w)=\infty }[/math].
в) Замена множества [math]\displaystyle{ F }[/math] на [math]\displaystyle{ P }[/math] и переход на шаг 3 в случае, если множество [math]\displaystyle{ F \neq \emptyset }[/math].
Схема реализации последовательного алгоритма
Простейшая версия алгоритма поиск в ширину может быть реализована при помощи очередей на языке C++ следующим образом. Код приведен в предположении, что граф хранится в формате сжатого списка смежности: для каждой вершины в массиве vertices_to_edges_ptrs хранятся индекс начала и индекс конца списка смежных с ней вершин из массива dst_ids.
// init distances
for(int i = 0; i < vertices_count; i++)
_result[i] = MAX_INT;
// init queue and first vertex
std::queue<int> vertex_queue;
vertex_queue.push(_source_vertex);
_result[_source_vertex] = 1;
// do bfs
while(vertex_queue.size() > 0)
{
int cur_vertex = vertex_queue.front();
vertex_queue.pop();
long long first = vertices_to_edges_ptrs[cur_vertex];
long long last = vertices_to_edges_ptrs[cur_vertex + 1];
for(long long i = first; i < last; i++)
{
int dst_id = dst_ids[i];
if(_result[dst_id] == MAX_INT)
{
_result[dst_id] = _result[src_id] + 1;
vertex_queue.push(dst_id);
}
}
}
Последовательная сложность алгоритма
Алгоритм имеет последовательную сложность [math]\displaystyle{ O(|V| + |E|) }[/math], где [math]\displaystyle{ |V| }[/math] и [math]\displaystyle{ |E| }[/math] - число вершин и ребер графа соответственно: алгоритм инициализирует начальный массив расстояний - [math]\displaystyle{ O(|V|) }[/math] операций, а затем обходит каждую вершину один единственный раз - [math]\displaystyle{ O(|E|) }[/math] операций. Данная оценка верна в случае, если формат хранения графа позволяет обходить вершины, смежные к выбранной (к примеру форматы списка смежности, сжатого списка смежности). При использовании других форматов оценка сложности может быть большей.
Информационный граф
Информационный граф классического алгоритма поиска в ширину приведен на рисунке 1.

На рисунке 1 используются следующие обозначения:
[1] - добавление вершины-источника [math]\displaystyle{ u }[/math] к множеству [math]\displaystyle{ F }[/math].
[2] - извлечение добавленной вершины [math]\displaystyle{ v }[/math] из множества [math]\displaystyle{ F }[/math].
[3] - проверка расстояний до вершин, смежных с вершиной [math]\displaystyle{ v }[/math].
[4] - добавление еще не посещенных вершин в множество [math]\displaystyle{ P }[/math].
[5] - замена множества [math]\displaystyle{ F }[/math] на [math]\displaystyle{ P }[/math] и проверка его пустоты. В случае непустого множества - переход на следующую итерацию, иначе завершение работы алгоритма.
Данный алгоритм имеет один важный недостаток при реализации: операция [4] требует бесконфликтной возможности добавления элементов в множество P, что, на практике, всегда будет сводиться к сериализации обращений к структуре данных, моделирующей данное множество.
В результате часто используется модификация алгоритма (далее алгоритм-2), использующая набор независимых структур данных для каждого из параллельных процессов. Информационный граф данного подхода приведен на рисунке 2.

Обозначения для рисунка 2:
[1] - добавление вершины-источника в множество [math]\displaystyle{ F }[/math].
[2] - разделение данных множества [math]\displaystyle{ F }[/math] между процессами
[3] - помещение в множества [math]\displaystyle{ F_i }[/math] соответствующих данных из [math]\displaystyle{ F }[/math] каждым процессом с номером i.
[4] - извлечение очередной вершины из множеств [math]\displaystyle{ F_i }[/math], обход её соседей и добавление их в множество [math]\displaystyle{ P_i }[/math] в случае, если они еще не посещены
[5] - попарное слияние множеств [math]\displaystyle{ P_i }[/math] для различных процессов, итоговое преобразование их в множество [math]\displaystyle{ F }[/math].
[6] - проверка условия выхода из цикла
Кроме того, в случае, если реализация структур данных, моделирующих множества [math]\displaystyle{ F }[/math] и [math]\displaystyle{ P }[/math], невозможна, может использоваться квадратичный по сложности алгоритм, схожий с алгоритм Беллмана-Форда. Основная идея заключается в том, что на каждом шаге производится обход всех ребер графа с обновлением текущего массива дистанций. Информационный граф данной модификации алгоритма приведен на рисунке 3.

Обозначения для рисунка 3:
[1] - инициализация расстояний до вершины-источника
[2] - инициализация расстояний до остальных вершин графа
[3] - загрузка информации об очередном ребре и обновление дистанций до соответствующих вершин.
[4] - проверка условия выхода из цикла
Ресурс параллелизма алгоритма
В ходе работы классический вариант алгоритма обходит граф по слоям. В каждый слой добавляются еще не посещенные вершины, достижимые из вершин предыдущего слоя. Обход вершин каждого слоя, как и их соседей, может производиться параллельно. Точно оценить число вершин в каждом слое невозможно в силу того, что их количество зависит от структуры связанности входного графа. Аналогично невозможно оценить число шагов алгоритма, за которое будут найдены все кратчайшие пути.
Произведем оценку ширины ярусно-параллельной формы алгоритма через максимальное число вершин p в слое среди всех шагов алгоритма. Тогда число параллельных операций на данном слое будет равно сумме числа смежных вершин для каждой вершины слоя: [math]\displaystyle{ \sum_{n=1}^{p} degree(v_i) }[/math], при этом для каждого слоя данное значение будет различным. Высота ярусно-параллельной формы будет равна числу шагов в алгоритме и может быть оценена только сверху (не более [math]\displaystyle{ |V| }[/math]).
При квадратичном подходе к параллельной реализации алгоритма на каждом шаге выполняется [math]\displaystyle{ O(|E|) }[/math] операций, которые могут быть выполнены параллельно; таким образом, ширина ЯПФ данной модификации алгоритма равна [math]\displaystyle{ O(|E|) }[/math]. Число шагов алгоритма, как и в классическом случае, зависит от структуры графа и может быть оценено сверху как [math]\displaystyle{ O(|V|) }[/math].
Входные и выходные данные алгоритма
Входные данные: граф [math]\displaystyle{ G(V, E) }[/math], [math]\displaystyle{ |V| }[/math] вершин [math]\displaystyle{ v_i }[/math] и [math]\displaystyle{ |E| }[/math] рёбер [math]\displaystyle{ e_j = (v^{(1)}_{j}, v^{(2)}_{j}) }[/math], вершина-источник [math]\displaystyle{ u }[/math].
Объём входных данных: [math]\displaystyle{ O(|V| + |E|) }[/math].
Выходные данные (возможные варианты):
- для каждой вершины [math]\displaystyle{ v }[/math] исходного графа расстояние [math]\displaystyle{ d(v) }[/math], определенное как число ребер, лежащих на кратчайшем пути от вершины [math]\displaystyle{ u }[/math] к [math]\displaystyle{ v }[/math].
- для каждой вершины [math]\displaystyle{ v }[/math] исходного графа значение достижимости (достижима или нет) от вершины-источника [math]\displaystyle{ u }[/math].
Объём выходных данных: [math]\displaystyle{ O(|V|) }[/math].