Графы
Как хранить графы?
Граф — $G=(V, E)$.
-
матрица смежности: + $O(1)$ на проверку $(i, j) \in E$ — Размер $\Theta(|V|^2)$ — $\Theta(|V|^2)$ на обход всех рёбер
-
список смежности + Размер $O(|V| + |E|)$ + Обход $O(|V| + |E|)$ — $O(|V|)$ на проверку $(i, j) \in E$
-
матрица инцидентности (почти не используется) + подходит для мультиграфов
Поиск в глубину
def Explore(v):
visited[v] = true
previsit(v)
for u : (v, u) in E # O(E)
if not visited[u]
Explore(v) # O(V) вызовов Explore
postvisit(v)
def DFS(G):
for i = 1 to |V|:
visited[i] = False
for v = 1 to |V|: # O(V)
if not visited[v]:
Explore(v)
Итого: $O(|V| + |E|)$ для списков смежности, и $O(|V|^2)$ для матриц смежности.
Компоненты связности в неориентированном графе
Как решить задачу с помощью $DFS$'a?
Заведём массив, который содержит компоненты вершин. То есть для каждой вершины мы храним, в какой компоненте она находится.
def CC(G):
for v = 1 to |V|:
CC[v] = 0
global counter = 0
for v = 1 to |V|:
counter += 1
if CC[v] == 0:
Explore(v)
def previsit:
CC[v] = counter
Поиск циклов в неориентированном графе
$DFS$ позволяет нам построить дерево по графу. Соответственно, если в процедуре Explore
вершина $v$ уже была посещена, это означает существование цикла.
Чтобы вернуть цикл, добавим в массив сначала вершину $v$, а потом будем возвращаться вверх по стеку вызовов, добавлять просматриваемую вершину и так до тех пор пока не не встретим вершину $v$.
Утверждение. В графе есть цикл $\Leftrightarrow$ в обходе $DFS$ есть обратное ребро.
Док-во. Если в графе есть цикл, рассмотрим цикл. Там возьмём первую вершину. Все остальные вершины рассмотрим тот же самый $Explore$, но одно ребро мы пройти не сможем (количество рёбер в дереве меньше чем вершин, а в цикле их столько же, то есть одно ребро останется), оно и смотрит наверх.
Обратно очевидно, если есть обратное ребро, то вот он цикл.
Поиск в глубину в ориентированном графе
Поиск циклов
см. картинку
- рёбра обхода
- прямые рёбра
- перекрёстные рёбра
- обратные рёбра
Утверждение. В орграфе есть цикл $\Leftrightarrow$ в обходе есть обратное ребро.
Будем записывать время заходи и исхода из вершин.
global counter = 1 # time
previsit(v):
pre[v] = counter
counter += 1
postvisit(v):
post[v] = counter
counter += 1
Утверждение. Для $\forall (u, v) \in E$ отрезки на числовой прямой $[\ pre[v],\ post[v]\ ]$ и $[\ pre[u],\ post[u]\ ]$ либо не пересекаются, либо один содержит другой. То есть не может быть такой картинки:
v *------------*
*----------* u
Рассмотрим различные соотношения отрезков для ребра $(u, v)$.
-
Ребро дерева / прямое ребро
u *------------* v *-----*
-
Обратное ребро
v *------------* u *-----*
-
Перекрёстное ребро. Обратное положение невозможно.
v *------* *-------* u
Таким образом, если мы найдём обратное ребро, мы найдём цикл.
Топологическая сортировка (всегда для ациклических графов)
Топологическая сортировка — это такой порядок вершин что: $\forall (u,v) \in E:\ \ T[i] < T[v]$.
По сути мы уже решили эту задачу, используя время выхода из вершин.
Утверждение. Вершина с максимальным $post$ — это вершина, которая ни от чего не зависит. Это исток (source, в неё не входит никакие рёбра).
Следствие. вершины в порядке убывания $post$ — это топологическая сортировка. (По индукции будем убирать вершину с максимальным post, и искать следующую с максимальным).