Графы

Как хранить графы?

Граф — $G=(V, E)$.

  1. матрица смежности: + $O(1)$ на проверку $(i, j) \in E$ — Размер $\Theta(|V|^2)$$\Theta(|V|^2)$ на обход всех рёбер

  2. список смежности + Размер $O(|V| + |E|)$ + Обход $O(|V| + |E|)$$O(|V|)$ на проверку $(i, j) \in E$

  3. матрица инцидентности (почти не используется) + подходит для мультиграфов

Поиск в глубину

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$, но одно ребро мы пройти не сможем (количество рёбер в дереве меньше чем вершин, а в цикле их столько же, то есть одно ребро останется), оно и смотрит наверх.

Обратно очевидно, если есть обратное ребро, то вот он цикл.

Поиск в глубину в ориентированном графе

Поиск циклов

см. картинку

  1. рёбра обхода
  2. прямые рёбра
  3. перекрёстные рёбра
  4. обратные рёбра

Утверждение. В орграфе есть цикл $\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)$.

  1. Ребро дерева / прямое ребро

     u *------------*
          v *-----*
    
  2. Обратное ребро

     v *------------*
          u *-----*
    
  3. Перекрёстное ребро. Обратное положение невозможно.

     v *------* *-------* u
    

Таким образом, если мы найдём обратное ребро, мы найдём цикл.

Топологическая сортировка (всегда для ациклических графов)

Топологическая сортировка — это такой порядок вершин что: $\forall (u,v) \in E:\ \ T[i] < T[v]$.

По сути мы уже решили эту задачу, используя время выхода из вершин.

Утверждение. Вершина с максимальным $post$ — это вершина, которая ни от чего не зависит. Это исток (source, в неё не входит никакие рёбра).

Следствие. вершины в порядке убывания $post$ — это топологическая сортировка. (По индукции будем убирать вершину с максимальным post, и искать следующую с максимальным).