mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-04 19:02:47 +08:00
548 lines
43 KiB
Markdown
548 lines
43 KiB
Markdown
# Графы
|
||
|
||
{width="3.5416732283464567in" height="4.583333333333333in"}
|
||
|
||
#### графы
|
||
|
||
> *Граф --* это нелинейная структура данных, состоящая из вершин и ребер. Граф *G* можно абстрактно представить как множество *вершин V* и множество *ребер E*. Ниже приведен пример графа, содержащего 5 вершин и 7 ребер:
|
||
>
|
||
> *V* = {1, 2, 3, 4, 5}
|
||
>
|
||
> *E* = {(1, 2), (1, 3), (1, 5), (2, 3), (2, 4), (2, 5), (4, 5)}
|
||
>
|
||
> *G* = {*V*, *E*}.
|
||
>
|
||
> Если рассматривать вершины как узлы, а ребра как ссылки (указатели), соединяющие узлы, то граф можно рассматривать как расширенный спи- сок. **По сравнению с линейными отношениями (список) и отношени- ями разделения (дерево), сетевые отношения (граф) обладают боль- шей свободой** и, следовательно, являются более сложными, как показано на рис. 9.1.
|
||
|
||

|
||
|
||
> **Рис. 9.1.** Связь между списком, деревом и графом
|
||
|
||
1. **Основные типы и понятия графов**
|
||
|
||
> В зависимости от наличия направления у ребер графы делятся на неориенти- рованные и ориентированные, как показано на рис. 9.2.
|
||
|
||
- В неориентированном графе ребро представляет собой двустороннюю связь между двумя вершинами, например дружеские отношения в соци- альных сетях.
|
||
|
||
- В ориентированном графе ребро имеет направление. То есть ребра *A* → *B* и *A* ← *B* независимы друг от друга, например отношения подписки--под- писчики.
|
||
|
||
> 
|
||
>
|
||
> **Рис. 9.2.** Ориентированный и неориентированный графы
|
||
>
|
||
> Если все вершины связаны, то граф называется связным, иначе -- несвяз- ным, как показано на рис. 9.3.
|
||
>
|
||
> В связном графе из любой вершины можно достичь любой другой вершины. В несвязном графе существуют по крайней мере две вершины, между кото-
|
||
>
|
||
> рыми нет пути.
|
||
|
||

|
||
|
||
> **Рис. 9.3.** Связный и несвязный графы
|
||
>
|
||
> Можно также добавить к ребрам переменную «вес», получив взвешенный граф, как показано на рис. 9.4. Например, в мобильных играх, таких как Honor of Kings, система рассчитывает близость между игроками на основе времени совместной игры. Такую сеть близости можно представить в виде взвешенного графа.
|
||
>
|
||
> Со структурой данных графа связаны следующие основные понятия.
|
||
|
||
- **Смежность**: если между двумя вершинами существует ребро, они на- зываются смежными. На рис. 9.4 вершины, смежные с вершиной 1, -- это вершины 2, 3 и 5.
|
||
|
||
- **Путь**: последовательность ребер от вершины *A* до вершины *B* называет- ся путем от *A* до *B*. На рис. 9.4 последовательность ребер 1-5-2-4 является путем от вершины 1 до вершины 4.
|
||
|
||
- **Степень**: количество ребер, присоединенных к вершине. Для ориен- тированного графа входящая степень показывает, сколько ребер ведет к данной вершине, а исходящая степень показывает, сколько ребер вы- ходит из данной вершины.
|
||
|
||
> 
|
||
>
|
||
> **Рис. 9.4.** Взвешенный и невзвешенный графы
|
||
|
||
### Представление графа
|
||
|
||
> Графы можно представить с помощью матрицы смежности и списка смежно- сти. Рассмотрим пример с неориентированным графом.
|
||
|
||
##### Матрица смежности
|
||
|
||
> Пусть количество вершин графа равно *n*, *матрица смежности* представля- ет граф в виде матрицы размером *n*×*n*, где каждая строка (столбец) соот- ветствует вершине, а элементы матрицы обозначают наличие ребра. Зна- чение 1 соответствует наличию ребра между двумя вершинами, значение 0 -- отсутствию.
|
||
>
|
||
> Обозначим матрицу смежности как *M*, а список вершин как *V*. Тогда элемент матрицы *M*\[*i*, *j*\] = 1 указывает на наличие ребра между вершинами *V*\[*i*\] и *V*\[*j*\], в противном случае элемент матрицы *M*\[*i*, *j*\] = 0, см. рис. 9.5.
|
||
|
||

|
||
|
||
> **Рис. 9.5.** Представление графа с помощью матрицы смежности
|
||
>
|
||
> Матрица смежности обладает следующими свойствами.
|
||
|
||
- В простом графе вершина не может быть соединена с самой собой, по- этому элементы на главной диагонали матрицы смежности не имеют значения.
|
||
|
||
- Для неориентированного графа ребра в обоих направлениях эквива- лентны, поэтому матрица смежности симметрична относительно глав- ной диагонали.
|
||
|
||
- Заменив элементы матрицы смежности с 1 и 0 на веса ребер, можно представить взвешенный граф.
|
||
|
||
> Используя матрицу смежности для представления графа, можно напрямую обращаться к элементам матрицы для получения информации о ребрах, что делает операции добавления, удаления, поиска и изменения достаточно эф- фективными с временной сложностью *O*(1). Однако пространственная слож- ность матрицы составляет *O*(*n*2), что требует значительных затрат памяти.
|
||
|
||
##### Список смежности
|
||
|
||
> *Список смежности* представляет граф с помощью *n* списков, где узлы списка представляют вершины. *i*-й список соответствует вершине *i* и содержит все смежные вершины (вершины, соединенные с данной вершиной). На рис. 9.6 показан пример графа, представленного с помощью списка смежности.
|
||
|
||

|
||
|
||
> **Рис. 9.6.** Представление графа с помощью списка смежности
|
||
>
|
||
> В списке смежности хранятся только существующие ребра, а общее количе- ство ребер обычно значительно меньше *n*2, что делает его более экономичным по памяти. Однако для поиска ребра в списке смежности необходимо просма- тривать список, что делает его менее эффективным по времени по сравнению с матрицей смежности.
|
||
|
||
###### Как видно из рис. 9.6, структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому можно использовать ана-
|
||
|
||
> **логичные методы для оптимизации эффективности**. Например, если список длинный, его можно преобразовать в АВЛ-дерево или красно-черное дерево, чтобы повысить временную эффективность с *O*(*n*) до *O*(log *n*). Так- же можно преобразовать список в хеш-таблицу, чтобы снизить временную сложность до *O*(1).
|
||
|
||
### Типичные сценарии применения графов
|
||
|
||
> Многие реальные системы можно моделировать с помощью графов, а соответ- ствующие задачи могут быть сведены к задачам вычисления на графах, см. табл. 9.1.
|
||
|
||
+---------------------+-----------------+-------------------------+------------------------------------------+
|
||
| | > **Вершина** | > **Ребро** | > **Задача вычисления на графе** |
|
||
+=====================+=================+=========================+==========================================+
|
||
| > Социальные сети | > Пользователи | > Дружеские связи | > Рекомендации потенци- альных друзей |
|
||
+---------------------+-----------------+-------------------------+------------------------------------------+
|
||
| > Линии метро | > Станции | > Связь между станциями | > Рекомендации по крат- чайшему маршруту |
|
||
+---------------------+-----------------+-------------------------+------------------------------------------+
|
||
| > Солнечная система | > Небесные тела | > Взаимодействие грави- | > Расчет орбит планет |
|
||
+---------------------+-----------------+-------------------------+------------------------------------------+
|
||
|
||
> тации между телами
|
||
|
||
#### ОСнОвные Операции С графами
|
||
|
||
> Основные операции с графами можно разделить на операции с ребрами и опе- рации с вершинами. В зависимости от способа представления (матрица смеж- ности или список смежности) реализация будет различаться.
|
||
|
||
### Реализация на основе матрицы смежности
|
||
|
||
> Ниже приведены операции для заданного неориентированного графа с коли- чеством вершин *n*. Способы реализации показаны на рис. 9.7.
|
||
|
||
- **Добавление или удаление ребра**: достаточно изменить соответствую- щее ребро в матрице смежности за время *O*(1). Поскольку граф неориен- тированный, необходимо обновить ребра в обоих направлениях.
|
||
|
||
- **Добавление вершины**: в конец матрицы смежности добавляется строка и столбец, которые заполняются нулями. Временная сложность равна *O*(*n*).
|
||
|
||
- **Удаление вершины**: удаляется строка и столбец из матрицы смеж- ности. В худшем случае при удалении первой строки и столбца не- обходимо переместить (*n* − 1)2 элементов влево вверх, что занимает время *O*(*n*2).
|
||
|
||
- **Инициализация**: передается *n* вершин, инициализируется список вер- шин vertices длиной *n* за время *O*(*n*). Инициализируется матрица смеж- ности adjMat размером *n*×*n* за время *O*(*n*2).
|
||
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.7.** Инициализация матрицы смежности, добавление и удаление ребер и вершин. Шаги 1--3
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.7.** *Окончание*. Шаги 4--5
|
||
>
|
||
> Ниже приведен код реализации графа на основе матрицы смежности.
|
||
>
|
||
> \# === File: graph_adjacency_matrix.py === class GraphAdjMat:
|
||
>
|
||
> \"\"\" Класс неориентированного графа на основе матрицы смежности.\"\"\"
|
||
>
|
||
> def init (self, vertices: list\[int\], edges: list\[list\[int\]\]): \"\"\" Конструктор.\"\"\"
|
||
>
|
||
> \# Список вершин, элемент представляет \"значение вершины\", индекс пред-
|
||
>
|
||
> ставляет \"индекс вершины\".
|
||
>
|
||
> self.vertices: list\[int\] = \[\]
|
||
>
|
||
> \# Матрица смежности, индексы строк и столбцов соответствуют \# \"индексу вершины\".
|
||
>
|
||
> self.adj_mat: list\[list\[int\]\] = \[\]
|
||
>
|
||
> \# Добавление вершин. for val in vertices:
|
||
>
|
||
> self.add_vertex(val) \# Добавление ребер.
|
||
>
|
||
> \# Обратите внимание: элементы edges представляют индексы вершин, \# т. е. соответствуют индексам элементов vertices.
|
||
>
|
||
> for e in edges: self.add_edge(e\[0\], e\[1\])
|
||
>
|
||
> def size(self) -\> int:
|
||
>
|
||
> \"\"\" Получение количества вершин.\"\"\" return len(self.vertices)
|
||
>
|
||
> def add_vertex(self, val: int): \"\"\" Добавление вершины.\"\"\" n = self.size()
|
||
>
|
||
> \# Добавление нового значения вершины в список вершин.
|
||
>
|
||
> self.vertices.append(val)
|
||
>
|
||
> \# Добавление строки в матрицу смежности. new_row = \[0\] \* n self.adj_mat.append(new_row)
|
||
>
|
||
> \# Добавление столбца в матрицу смежности.
|
||
>
|
||
> for row in self.adj_mat: row.append(0)
|
||
>
|
||
> def remove_vertex(self, index: int): \"\"\" Удаление вершины.\"\"\"
|
||
>
|
||
> if index \>= self.size(): raise IndexError()
|
||
>
|
||
> \# Удаление вершины с индексом index из списка вершин.
|
||
>
|
||
> self.vertices.pop(index)
|
||
>
|
||
> \# Удаление строки с индексом index из матрицы смежности. self.adj_mat.pop(index)
|
||
>
|
||
> \# Удаление столбца с индексом index из матрицы смежности.
|
||
>
|
||
> for row in self.adj_mat: row.pop(index)
|
||
>
|
||
> def add_edge(self, i: int, j: int): \"\"\" Добавление ребра.\"\"\"
|
||
>
|
||
> \# Параметры i и j соответствуют индексам элементов vertices. \# Обработка выхода за границы индексов и равенства.
|
||
>
|
||
> if i \< 0 or j \< 0 or i \>= self.size() or j \>= self.size() or i == j: raise IndexError()
|
||
>
|
||
> \# В неориентированном графе матрица смежности симметрична
|
||
>
|
||
> \# относительно главной диагонали, т. е. (i, j) == (j, i). self.adj_mat\[i\]\[j\] = 1
|
||
>
|
||
> self.adj_mat\[j\]\[i\] = 1
|
||
>
|
||
> def remove_edge(self, i: int, j: int): \"\"\" Удаление ребра.\"\"\"
|
||
>
|
||
> \# Параметры i и j соответствуют индексам элементов vertices.
|
||
>
|
||
> \# Обработка выхода за границы индексов и равенства.
|
||
>
|
||
> if i \< 0 or j \< 0 or i \>= self.size() or j \>= self.size() or i == j: raise IndexError()
|
||
>
|
||
> self.adj_mat\[i\]\[j\] = 0
|
||
>
|
||
> self.adj_mat\[j\]\[i\] = 0
|
||
>
|
||
> def print(self):
|
||
>
|
||
> \"\"\" Печать матрицы смежности.\"\"\"
|
||
>
|
||
> print(\" Список вершин =\", self.vertices) print(\" Матрица смежности =\") print_matrix(self.adj_mat)
|
||
|
||
### Реализация на основе списка смежности
|
||
|
||
> Ниже приведены описания операций для неориентированного графа с общим количеством вершин *n* и ребер *m*. Способы реализации показаны на рис. 9.8.
|
||
|
||
- **Добавление ребра**: достаточно добавить ребро в конец связного списка, соответствующего вершине за время *O*(1). Поскольку граф неориентиро- ванный, необходимо добавить ребра в обоих направлениях.
|
||
|
||
- **Удаление ребра**: необходимо найти и удалить указанное ребро в связ- ном списке, соответствующем вершине, за время *O*(*m*). В неориентиро- ванном графе необходимо удалить ребра в обоих направлениях.
|
||
|
||
- **Добавление вершины**: добавляется связный список в список смеж- ности, а новая вершина становится головным узлом списка. Требуется время *O*(1).
|
||
|
||
- **Удаление вершины**: необходимо пройтись по всему списку смежности и удалить все ребра, содержащие указанную вершину. Требуется время *O*(*n* + *m*).
|
||
|
||
- **Инициализация**: в списке смежности создается *n* вершин и 2*m* ребер за время O(*n* + *m*).
|
||
|
||

|
||
|
||
> **Рис. 9.8.** Инициализация списка смежности, добавление и удаление ребер и вершин.
|
||
>
|
||
> Шаг 1
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.8.** *Продолжение*. Шаг 2--4
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 9.8.** *Окончание*. Шаг 5
|
||
>
|
||
> Ниже приведен код реализации списка смежности. По сравнению с рис. 9.8 код имеет следующие отличия:
|
||
|
||
- для удобства добавления и удаления вершин, а также упрощения кода вместо связного списка используется список (динамический массив);
|
||
|
||
- для хранения списка смежности используется хеш-таблица, где ключом является экземпляр вершины, а значением -- список смежных вершин (связный список).
|
||
|
||
> Кроме того, в списке смежности используется класс Vertex для представле- ния вершин. Это сделано потому, что если, как в случае с матрицей смежности, использовать индексы списка для различения различных вершин, то при уда- лении вершины с индексом *i* необходимо пройтись по всему списку смежно- сти и уменьшить на 1 все индексы, большие *i*, что крайне неэффективно. Если же каждая вершина является уникальным экземпляром класса Vertex, то после удаления одной вершины не требуется изменять другие вершины.
|
||
>
|
||
> \# === File: graph_adjacency_list.py === class GraphAdjList:
|
||
>
|
||
> \"\"\" Класс неориентированного графа на основе списка смежности.\"\"\"
|
||
>
|
||
> def init (self, edges: list\[list\[Vertex\]\]): \"\"\" Конструктор.\"\"\"
|
||
>
|
||
> \# Список смежности, ключ: вершина, значение: все смежные вершины данной
|
||
>
|
||
> вершины.
|
||
>
|
||
> self.adj_list = dict\[Vertex, list\[Vertex\]\]() \# Добавление всех вершин и ребер.
|
||
>
|
||
> for edge in edges: self.add_vertex(edge\[0\]) self.add_vertex(edge\[1\]) self.add_edge(edge\[0\], edge\[1\])
|
||
>
|
||
> def size(self) -\> int:
|
||
>
|
||
> \"\"\" Получение количества вершин.\"\"\" return len(self.adj_list)
|
||
>
|
||
> def add_edge(self, vet1: Vertex, vet2: Vertex): \"\"\" Добавление ребра.\"\"\"
|
||
>
|
||
> if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2: raise ValueError()
|
||
>
|
||
> \# Добавление ребра vet1 - vet2 self.adj_list\[vet1\].append(vet2) self.adj_list\[vet2\].append(vet1)
|
||
>
|
||
> def remove_edge(self, vet1: Vertex, vet2: Vertex): \"\"\" Удаление ребра.\"\"\"
|
||
>
|
||
> if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2: raise ValueError()
|
||
>
|
||
> \# Удаление ребра vet1 - vet2. self.adj_list\[vet1\].remove(vet2) self.adj_list\[vet2\].remove(vet1)
|
||
>
|
||
> def add_vertex(self, vet: Vertex): \"\"\" Добавление вершины.\"\"\"
|
||
>
|
||
> if vet in self.adj_list: return
|
||
>
|
||
> \# В списке смежности добавляется новый список. self.adj_list\[vet\] = \[\]
|
||
>
|
||
> def remove_vertex(self, vet: Vertex): \"\"\" Удаление вершины.\"\"\"
|
||
>
|
||
> if vet not in self.adj_list: raise ValueError()
|
||
>
|
||
> \# В списке смежности удаляется список, соответствующий вершине vet. self.adj_list.pop(vet)
|
||
>
|
||
> \# Обход списков других вершин, удаление всех ребер, содержащих vet. for vertex in self.adj_list:
|
||
>
|
||
> if vet in self.adj_list\[vertex\]: self.adj_list\[vertex\].remove(vet)
|
||
>
|
||
> def print(self):
|
||
>
|
||
> \"\"\" Печать списка смежности.\"\"\" print(\" Список смежности =\") for vertex in self.adj_list:
|
||
>
|
||
> tmp = \[v.val for v in self.adj_list\[vertex\]\] print(f\"{vertex.val}: {tmp},\")
|
||
|
||
### Сравнение эффективности
|
||
|
||
> Пусть дан граф с *n* вершинами и *m* ребрами. В табл. 9.2 приведено сравне- ние временной и пространственной сложности матрицы смежности и списка смежности.
|
||
>
|
||
> **Таблица 9.2.** Сравнение матрицы и списка смежности
|
||
|
||
+---------------------------------------------------------------------------------------------------------------+
|
||
| > **Операция Матрица Список смежности Список смежности смежности (связный список) (хеш-таблица)** |
|
||
+===========================+===========================+===========================+===========================+
|
||
| > Проверка смежности | > *O*(1) | > *O*(*m*) | > *O*(1) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
| > Добавление ребра | > *O*(1) | > *O*(1) | > *O*(1) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
| > Удаление ребра | > *O*(1) | > *O*(*m*) | > *O*(1) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
| > Добавление вершины | > *O*(*n*) | > *O*(1) | > *O*(1) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
| > Удаление вершины | > *O*(*n*²) | > *O*(*n* + *m*) | > *O*(*n*) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
| > Занимаемое пространство | > *O*(*n*²) | > *O*(*n* + *m*) | > *O*(*n* + *m*) |
|
||
+---------------------------+---------------------------+---------------------------+---------------------------+
|
||
|
||
> Из табл. 9.2 видно, что временная и пространственная эффективность спи- ска смежности (хеш-таблица) наиболее оптимальна. Однако на практике опе- рации с ребрами в матрице смежности более эффективны, так как требуют лишь одного доступа или присвоения в массиве. В целом матрица смежности реализует принцип обмена пространства на время, тогда как список смежно- сти -- обмена времени на пространство.
|
||
|
||
#### Обход графа
|
||
|
||
> Дерево представляет собой отношение «один ко многим», тогда как граф обла- дает большей степенью свободы и может представлять произвольные отноше- ния «многие ко многим». Таким образом, **дерево можно рассматривать как частный случай графа**. Очевидно, что операции обхода дерева также являют- ся частным случаем обхода графа.
|
||
>
|
||
> И графы, и деревья требуют применения алгоритмов поиска для реализации операций обхода. Способы обхода графа можно разделить на два типа: обход в ширину и обход в глубину.
|
||
|
||
### Обход в ширину
|
||
|
||
> **Обход в ширину (BFS)** -- **это метод обхода от ближнего к дальнему**, **начи- ная с определенного узла**, **с посещением в первую очередь ближайших вершин с постепенным расширением наружу**. Начиная с левого верхнего угла, сначала обходятся все смежные вершины текущей вершины, затем все смежные вершины следующей вершины и т. д., пока не будут посещены все вершины, как показано на рис. 9.9.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 9.9.** Обход графа в ширину
|
||
|
||
##### Реализация алгоритма
|
||
|
||
> Обход в ширину обычно реализуется с помощью очереди, код реализации при- веден ниже. Очередь обладает свойством «первый пришел -- первый вышел», что соответствует идее обхода в ширину «от ближнего к дальнему». Алгоритм следующий:
|
||
|
||
1) добавить начальную вершину обхода startVet в очередь и начать цикл;
|
||
|
||
2) на каждой итерации цикла извлекать вершину из головы очереди и за- писывать посещение, затем добавлять все смежные вершины этой вер- шины в хвост очереди;
|
||
|
||
3) повторять шаг 2, пока не будут посещены все вершины.
|
||
|
||
> Чтобы избежать повторного обхода вершин, необходимо использовать хеш- множество visited для записи посещенных узлов.
|
||
>
|
||
> \# === File: graph_bfs.py ===
|
||
>
|
||
> def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -\> list\[Vertex\]: \"\"\" Обход в ширину.\"\"\"
|
||
>
|
||
> \# Использование списка смежности для представления графа, чтобы получить
|
||
>
|
||
> все смежные вершины текущей вершины.
|
||
>
|
||
> \# Последовательность обхода вершин. res = \[\]
|
||
>
|
||
> \# Хеш-множество для записи уже посещенных вершин. visited = set\[Vertex\](\[start_vet\])
|
||
>
|
||
> \# Очередь для реализации поиска в ширину. que = deque\[Vertex\](\[start_vet\])
|
||
>
|
||
> \# Начало с вершины vet; цикл до тех пор, пока не будут посещены все вершины. while len(que) \> 0:
|
||
>
|
||
> vet = que.popleft() \# Вершина извлекается из головы очереди. res.append(vet) \# Запись посещенной вершины.
|
||
>
|
||
> \# Обход всех смежных вершин этой вершины. for adj_vet in graph.adj_list\[vet\]:
|
||
>
|
||
> if adj_vet in visited:
|
||
>
|
||
> continue \# Пропуск уже посещенных вершин. que.append(adj_vet) \# В очередь добавляются только
|
||
>
|
||
> \# непосещенные вершины. visited.add(adj_vet) \# Отметка, что вершина была посещена.
|
||
>
|
||
> \# Возврат последовательности обхода вершин. return res
|
||
>
|
||
> Код относительно абстрактен, рекомендуется обратиться к рис. 9.10 для более глубокого понимания.
|
||
|
||

|
||
|
||
> **Рис. 9.10.** Этапы обхода графа в ширину. Шаг 1
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.10.** *Продолжение*. Шаги 2--3
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.10.** *Продолжение*. Шаги 4--5
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.10.** *Продолжение*. Шаги 6--7
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.10.** *Продолжение*. Шаги 8--9
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
##### Анализ сложности
|
||
|
||
> **Рис. 9.10.** *Окончание*. Шаги 10--11
|
||
>
|
||
> **Временная сложность**: все вершины будут добавляются в очередь и удаляют- ся из нее ровно один раз, что требует времени *O*(\|*V*\|). В процессе обхода смеж- ных вершин, поскольку граф неориентированный, все ребра будут посещены дважды, что занимает время *O*(2\|*E*\|). В целом требуется время *O*(\|*V*\| + \|*E*\|).
|
||
>
|
||
> **Пространственная сложность**: список res, хеш-множество visited и количе- ство вершин в очереди que максимум равны \|*V*\|, что требует пространства *O*(\|*V*\|).
|
||
|
||
### Обход в глубину
|
||
|
||
> **Обход в глубину (DFS)** -- **это метод обхода, при котором сначала ис- следуются все возможные пути до самого конца**, **а затем происходит возврат**. Начиная с левой верхней вершины, посещается какая-либо смеж- ная вершина текущей вершины, пока не будет достигнут конец пути, после чего происходит возврат и опять продолжается обход до конца. Продолжа- ем процесс и так далее, пока все вершины не будут посещены, как показано на рис. 9.11.
|
||
|
||

|
||
|
||
> **Рис. 9.11.** Обход графа в глубину
|
||
|
||
##### Реализация алгоритма
|
||
|
||
> Этот алгоритмический подход «до конца и назад» обычно реализуется с помо- щью рекурсии. Подобно обходу в ширину, в обходе в глубину также необходи- мо использовать хеш-множество visited для записи уже посещенных вершин, чтобы избежать их повторного посещения.
|
||
>
|
||
> \# === File: graph_dfs.py ===
|
||
>
|
||
> def dfs(graph: GraphAdjList, visited: set\[Vertex\], res: list\[Vertex\], vet: Ver- tex):
|
||
>
|
||
> \"\"\" Вспомогательная функция для обхода в глубину.\"\"\" res.append(vet) \# Запись посещенной вершины. visited.add(vet) \# Пометка вершины как посещенной. \# Обход всех смежных вершин текущей вершины.
|
||
>
|
||
> for adjVet in graph.adj_list\[vet\]: if adjVet in visited:
|
||
>
|
||
> continue \# Пропуск уже посещенной вершины. \# Рекурсивное посещение смежной вершины. dfs(graph, visited, res, adjVet)
|
||
>
|
||
> def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -\> list\[Vertex\]: \"\"\" Обход в глубину.\"\"\"
|
||
>
|
||
> \# Использование списка смежности для представления графа, чтобы получить все смежные вершины текущей вершины.
|
||
>
|
||
> \# Последовательность обхода вершин. res = \[\]
|
||
>
|
||
> \# Хеш-множество для записи уже посещенных вершин.
|
||
>
|
||
> visited = set\[Vertex\]()
|
||
>
|
||
> dfs(graph, visited, res, start_vet) return res
|
||
>
|
||
> Алгоритм обхода в глубину показан на рис. 9.12.
|
||
|
||
- **Прямые пунктирные линии** обозначают нисходящую рекурсию, ука- зывая на начало нового рекурсивного метода для посещения новой вер- шины.
|
||
|
||
- **Изогнутые пунктирные линии** обозначают восходящую рекурсию, указывая на возврат данного рекурсивного метода к месту его начала.
|
||
|
||
> Для лучшего понимания рекомендуется на примере рис. 9.12 и кода реали- зации мысленно (или с помощью рисунка) смоделировать весь процесс обхода в глубину, включая моменты начала и возврата каждого рекурсивного метода.
|
||
|
||

|
||
|
||
> **Рис. 9.12.** Этапы обхода графа в глубину. Шаги 1--2
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.12.** *Продолжение*. Шаги 3--5
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.12.** *Продолжение*. Шаги 6--8
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 9.12.** *Окончание*. Шаги 9--11
|
||
|
||
##### Анализ сложности
|
||
|
||
> **Временная сложность**: все вершины будут посещены один раз, что требует времени *O*(\|*V*\|). Все ребра будут посещены дважды, что требует времени *O*(2\|*E*\|). В целом требуется время *O*(\|*V*\| + \|*E*\|).
|
||
>
|
||
> **Пространственная сложность**: список res и хеш-множество visited имеют максимум \|*V*\| вершин, максимальная глубина рекурсии равна \|*V*\|, следователь- но, требуется пространство *O*(\|*V*\|).
|
||
|
||
#### резюме
|
||
|
||
##### Ключевые моменты
|
||
|
||
- Граф состоит из вершин и ребер, его можно задать как множество вер- шин и множество ребер.
|
||
|
||
- По сравнению с линейными отношениями (список) и отношениями раз- деления (дерево), сетевые отношения (граф) обладают большей степе- нью свободы и, следовательно, более сложны.
|
||
|
||
- Ребра ориентированного графа имеют направленность, в связном графе любые вершины достижимы, во взвешенном графе каждое ребро содер- жит переменную веса.
|
||
|
||
- Матрица смежности использует матрицу для представления графа, каж- дая строка (столбец) представляет вершину, элементы матрицы пред- ставляют ребра. Значение 1 соответствует наличию ребра между двумя вершинами, значение 0 -- отсутствию. Матрица смежности эффектив- на в операциях добавления, удаления, поиска и изменения, но требует больше пространства.
|
||
|
||
- Список смежности использует несколько списков для представления графа, *i*-й список соответствует вершине *i* и хранит все смежные верши- ны этой вершины. Список смежности экономнее по сравнению с матри- цей смежности, но из-за необходимости обхода списка для поиска ребра его временная эффективность ниже.
|
||
|
||
- Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу для по- вышения эффективности поиска.
|
||
|
||
> 9.4. Резюме ❖ **263**
|
||
|
||
- С точки зрения алгоритмических подходов матрица смежности реализу- ет принцип обмена пространства на время, а список смежности -- обмена времени на пространство.
|
||
|
||
- Графы используются для моделирования различных реальных систем, таких как социальные сети, линии метро и т. д.
|
||
|
||
- Дерево является частным случаем графа, а обход дерева -- частным слу- чаем обхода графа.
|
||
|
||
- Обход графа в ширину (BFS) представляет собой метод поиска, который расширяется от ближних к дальним уровням, обычно реализуется с по- мощью очереди.
|
||
|
||
- Обход графа в глубину (DFS) -- это метод поиска, который сначала прохо- дит до конца, а затем отступает назад, когда дальнейшего пути нет, часто реализуется на основе рекурсии.
|
||
|
||
##### Вопросы и ответы
|
||
|
||
> **Вопрос**. Путь -- это последовательность вершин или ребер?
|
||
>
|
||
> **Ответ**. В разных языковых версиях «Википедии» определения различа- ются: в английской версии путь -- это последовательность ребер, а в русской версии путь -- это последовательность вершин. Приведем оригинальный текст английской версии: In graph theory, a path in a graph is a finite or infinite se- quence of edges which joins a sequence of vertices.
|
||
>
|
||
> В этой книге путь рассматривается как последовательность ребер, а не вер- шин. Это связано с тем, что между двумя вершинами может существовать не- сколько соединяющих ребер, и каждое из них соответствует отдельному пути. **Вопрос**. Могут ли существовать в несвязном графе недостижимые вершины?
|
||
>
|
||
> **Ответ**. В несвязном графе существует по крайней мере две вершины -- та- кие, что одна не достижима из другой. Для обхода несвязного графа необхо- димо установить несколько начальных точек, чтобы обойти все связные ком- поненты графа.
|
||
>
|
||
> **Вопрос**. Существует ли в списке смежности требование к выбору порядка всех вершин, связанных с данной вершиной?
|
||
>
|
||
> **Ответ**. Порядок может быть произвольным. Однако на практике может по- требоваться сортировка по определенным правилам, например в порядке до- бавления вершин или в порядке значений вершин, что помогает быстро на- ходить вершины с определенным экстремумом.
|
||
>
|
||
> Глава 10
|