Поиск в ширину. Алгоритмы графов.
5 января 2024 г.
Это краткий дополненный конспект части о поиске в ширину в книге Гид по Computer Science, Вильям Спрингер.
Основная информация
Алгоритм поиска в ширину (Breadth-First Search, BFS) превращает произвольный граф в дерево поиска.
- 1) назначаем одну из вершин графа корнем дерева
- 2) рекурсивно исследуем ребра каждой вершины, пока все вершины графа (любая вершина, которой можно достичь, переходя по ребрам от корня; в случае связного графа это будут все вершины) не будут добавлены в остовное дерево (дерево, которое содержит все вершины графа).
При анализе времени выполнения алгоритмов на графах обычно используются две системы обозначений:
1) обозначение числа вершин n, а числа ребер — m.
2) указать размеры множеств вершин и ребер.
Например, Для графа G множество вершин обозначается как V(G) [читается «вершины G»), а множество ребер — как E(G) (читается «ребра G»), поэтому n = |V(G)| (можно прочитать как «размер множества вершин G»), а m = |E(G)|. Если G подразумевается по умолчанию, то можно обращаться к множествам просто как V и E.
Алгоритм:
- 1) Исследуем все вершины, примыкающие к корню (на расстоянии k от корня. Сам корень обозначается s (source — «источник»));
- 2) Исследуем все вершины, смежные с соседями корня, которые еще не были исследованы (на расстоянии k + 1) и т.д.
При завершении алгоритма, глубина каждого узла дерева — минимальное количество ребер (длина кратчайшего пути), по которому можно добраться до этого узла из s как в дереве, так и в исходном графе. Чтобы найти этот путь, нужно идти по указателям на родительские узлы, пока не будет достигнут узел s.
Время выполнения - O(n + m).
- инициализация трех свойств (упомянутых выше) - постоянное время для каждой вершины. В сумме - O(n).
- каждая вершина добавляется в очередь, удаляется из очереди, и каждое из свойств изменяется не более одного раза — каждая из операций O(1) времени, в сумме O(n).
- перебираем всех соседей каждой вершины, чтобы проверить, отмечены ли они - O(m)
Место в памяти - O(n + m)
- хранения графа - n + m места в памяти,
- очередь занимает O(n) места
По отношению к размеру входных данных работает за линейное время.
1// Входные данные: произвольный граф, корнем которого выбрана одна вершина, и все вершин имеют такие свойства:23// distance (расстояние) - изначально === infinity45// parent (родитель) - изначально === 067// marked (помечена) - изначально === false89// Выходные данные: остовное дерево графа. Его каждая вершина максимально приближена к корню.1011begin12 let queue = [];13 s.distance = 0;14 s.marked = true;15 queue.push(s);16 while q.length > 0 do17 let activeVertex = queue.shift();18 foreach v in Adj[activeVertex] do19 if v.marked then20 continue21 end22 v.parent = activeVertex;23 v.distance = activeVertex.distance + 124 v.marked = true25 queue.push(v)26 end27 end28end2930//где Adj - граф, представленный в виде списка смежности.
Реализация на JavaScript
Оба примера взяты с этого сайта
Пример для реализации графа.
1class Graph {2 constructor() {3 this.vertices = {}; // список смежности графа4 }56 addVertex(value) {7 if (!this.vertices[value]) {8 this.vertices[value] = [];9 }10 }1112 addEdge(vertex1, vertex2) {13 if (!(vertex1 in this.vertices) || !(vertex2 in this.vertices)) {14 throw new Error("В графе нет таких вершин");15 }1617 if (!this.vertices[vertex1].includes(vertex2)) {18 this.vertices[vertex1].push(vertex2);19 }20 if (!this.vertices[vertex2].includes(vertex1)) {21 this.vertices[vertex2].push(vertex1);22 }23 }2425 bfs(startVertex, callback) {26 let list = this.vertices; // список смежности27 let queue = [startVertex]; // очередь вершин для перебора28 let visited = { [startVertex]: 1 }; // посещенные вершины2930 function handleVertex(vertex) {31 // вызываем коллбэк для посещенной вершины32 callback(vertex);3334 // получаем список смежных вершин35 let neighboursList = list[vertex];3637 neighboursList.forEach((neighbour) => {38 if (!visited[neighbour]) {39 visited[neighbour] = 1;40 queue.push(neighbour);41 }42 });43 }4445 // перебираем вершины из очереди, пока она не опустеет46 while (queue.length) {47 let activeVertex = queue.shift();48 handleVertex(activeVertex);49 }5051 queue = Object.keys(this.vertices);5253 // Повторяем цикл для незатронутых вершин54 while (queue.length) {55 let activeVertex = queue.shift();56 if (!visited[activeVertex]) {57 visited[activeVertex] = 1;58 handleVertex(activeVertex);59 }60 }61 }62}6364const graph = new Graph();6566graph.addVertex("A");67graph.addVertex("B");68graph.addVertex("C");69graph.addVertex("D");70graph.addVertex("E");71graph.addVertex("F");72graph.addVertex("G");73graph.addVertex("H");7475graph.addEdge("A", "B");76graph.addEdge("A", "C");77graph.addEdge("C", "D");78graph.addEdge("C", "E");79graph.addEdge("A", "F");80graph.addEdge("F", "G");8182graph.bfs('A', v => console.log(v));8384// A85// B86// C87// F88// D89// E90// G91// H
Пример для реализации дерева.
1// Возьмем простой пример реализации очереди.2class Queue {3 constructor() {4 this.arr = [];5 }6 enqueue(value) {7 this.arr.push(value);8 }9 dequeue() {10 return this.arr.shift();11 }12 isEmpty() {13 return this.arr.length == 0;14 }15}1617class BinaryTreeNode {18 constructor(value) {19 this.left = null;20 this.right = null;21 this.parent = null;22 this.value = value;23 }2425 get height() {26 let leftHeight = this.left ? this.left.height + 1 : 0;27 let rightHeight = this.right ? this.right.height + 1 : 0;28 return Math.max(leftHeight, rightHeight);29 }3031 //При вставке важно обновить все затронутые ссылки: left или right для родительского узла и parent для дочернего. Если у родителя уже был потомок, его свойство parent нужно обнулить.3233 setLeft(node) {34 if (this.left) {35 this.left.parent = null;36 }37 if (node) {38 this.left = node;39 this.left.parent = this;40 }41 }4243 setRight(node) {44 if (this.right) {45 this.right.parent = null;46 }47 if (node) {48 this.right = node;49 this.right.parent = this;50 }51 }52}5354function traverseBF(root, callback) {55 let nodeQueue = new Queue();56 nodeQueue.enqueue(root);5758 while (!nodeQueue.isEmpty()) {59 let currentNode = nodeQueue.dequeue();6061 // Вызываем коллбэк для самого узла62 callback(currentNode);6364 // Добавляем в очередь левого потомка65 if (currentNode.left) {66 nodeQueue.enqueue(currentNode.left);67 }6869 // Добавляем в очередь правого потомка70 if (currentNode.right) {71 nodeQueue.enqueue(currentNode.right);72 }73 }74}7576let aNode = new BinaryTreeNode('a');7778let bNode = new BinaryTreeNode('b');79aNode.setLeft(bNode);8081let cNode = new BinaryTreeNode('c');82aNode.setRight(cNode);8384let dNode = new BinaryTreeNode('d');85bNode.setRight(dNode);8687let eNode = new BinaryTreeNode('e');88cNode.setLeft(eNode);8990let fNode = new BinaryTreeNode('f');91cNode.setRight(fNode);9293traverseBF(aNode, (node) => console.log(node.value));9495// a96// b97// c98// d99// e100// f
Применение поиска в ширину
Полезен для всех задач, где нужен поиск кратчайших путей.
- Позволяет найти длину кратчайшего пути от исходной вершины s до любой другой вершины графа за линейное время.
- Можем найти сам путь, следуя по родительским указателям от исходного узла до корня дерева, за время, пропорциональное длине пути.
Примеры задач
Навигационные системы
Задача построения маршрутов с помощью GPS.
Если картографическая система хранит данные о локальной области в виде графа, вершины которого - адреса (или пересечения), а ребра — улицы (или, точнее, короткие отрезки улиц), то она может выполнять поиск в ширину для дерева, источник которого — ваше текущее местоположение.
Проверка, является ли граф двудольным
Граф не двудольный - если существует ребро между данной вершиной и уже отмеченной вершиной, расстояние которой либо такое же, либо меньше в степени 2. Этим вершинам будет присвоен один и тот же цвет.
Еще один признак не двудольного графа - ребро вместе с одним или несколькими путями к наименьшему общему предку двух вершин - нечетный цикл.
Двудо́льный граф или бигра́ф в теории графов — это граф, множество вершин которого можно разбить на две части таким образом, что каждое ребро графа соединяет вершину из одной части с какой-то вершиной другой части, то есть не существует рёбер между вершинами одной и той же части графа
Двудольные графы используются в теории кодирования (теория кодирования изучает, в частности, криптографию, сжатие данных и исправление ошибок), в сетях Петри (сети Петри используются для моделирования поведения систем), в анализе социальных сетей и облачных вычислениях.