Strongly connected components

Утверждения:

Компонента сильной связности — это максимальное подвключение подмножества $V$ такое, что $\forall u, v \in V$ существует путь $u\rightarrow v \rightarrow u$.

Метаграф — граф, в котором все компоненты сильной связности заменены на единичные вершины (с сохранением ребер между компонентами). Если есть несколько ребер между двумя компонентами, то, в зависимости от задачи, можно либо их склеить в одно ребро, либо оставить все.

Утверждение: — метаграф ациклический. Доказательство: — если бы в нем был цикл, то несколько компонент сильной связности склеились бы в одну.

Утверждение: — есть две вершины $U$ и $V$ (в метаграфе), которые задают две компоненты сильной связности, $(U \rightarrow V) \in H$ ($H$ — множество ребер метаграфа). $\max\limits_{u \in U}\{post[u]\} > \max\limits_{v \in V}\{post[v]\}$ ($post$ — время выхода из вершины при обходе DFS). Доказательство: 1. Сначала попали в $U \rightarrow$ ОК (у одной из вершин $u$ время выхода будет больше, чем для всех $v$) 2. Сначала попали в $V \rightarrow$ не попали в $U$ (туда попадем только на следующем вызове DFS $\rightarrow$ для всех $u$ время выхода больше любого времени $v$).

Утверждение: вершина с максимальным $post$ лежит в компоненте истоке.

Утверждение: если отсортировать вершины по убыванию $post$, то получится топологическая сортировка на метаграфе.

Алгоритм: отсортируем вершины по убыванию $post$ и запустим обычный поиск компонент связности в перевернутом графе.

def scc(g):
    dfs(g)
    g_r = revert(g)
    sort(g_r.v, maxpost, reverse=True)  # сортируем вершины графа g_r
    # можно получить отсортированный массив прямо внутри dfs
    cc(g_r)  # поиск компонент

Время работы $O(|V| + |E|)$ (можно использовать сортировку подсчетом, что сортировать за $O(|V|)$).

Кратчайшие пути

Поиск в ширину

def bfs(g, w):  # w — вершина, откуда запускаем обход
    q = deque()
    dist = [inf for v in g.f]  # массив расстояний
    prev = [0 for v in g.f]    # восстановление пути
    dist[w] = 0
    q.append(v)
    while q:
        v = q.popleft()
        for u in g.v[v].neighbours:
            if dist[u] < inf:
                dist[u] = dist[v] + 1
                prev[u] = v
                q.append(u)
    return dist, prev

Время работы $O(|V| + |E|)$

Утверждение: $\forall u: \ d[u] = \text{dist}(w, u)$ База: $u=w \Rightarrow d[w] = \text{dist}(w, w) = 0$ И.п.: верно на расстояниях $\leq k \Rightarrow$ верно на расстоянии $k+1$. Все вершины на расстоянии $k+1$ являются соседями вершин на расстоянии $k$ (достаточно очевидно).

Утверждение: пусть $\Pi = (v, v_1, v_2,\ ...\ ,v_k, u)$ — кратчайший путь $v \rightarrow u$. Тогда для $\forall i, j: \ i < j:\ \Pi'=(v_i, v_{i+1},\ ...\ , v_j)$ — кратчайший путь $v_i\rightarrow v_j$.

BFS Не работает для поиска кратчайшего пути, если граф взвешенный.

Можно добавить фиктивных вершин (ребро веса 5 — четыре дополнительных вершины веса 1 на это ребро). Минусы:

Алгоритм Дейкстры

Работает для графов с неотрицательными ребрами.

Для реализации нужна очередь с приоритетами с операциями:

def dijkstra(g, s):  # s — стартовая вершина
    dist = [inf for v in g.v]
    prev = [0 for  in g.v]
    dist[s] = 0
    q = priority_queue()
    q.insert(s, 0)
    while q:
        v = q.extract_min()
        for u in g.v[v].neighbours:
            if dist[u] == inf:
                dist[u] = dist[v] + g.w[v]
                prev[u] = v
                q.insert(u, dist[u])
            elif dist[u] > dist[v] + g.w[v]:
                dist[u] = dist[v] + g.w[v]
                prev[u] = v
                q.change_priority(u, dist[u])
            # если запилить такую очередь, что каждое value
            # представлено только 1 раз, и q.insert в случае
            # чего перезапишет старый приоритет, то можно
            # обойтись только одним if'ом
    return dist, prev

Лемма: алгоритм Дейскстры корректен. $\forall u: \ d[u] = \text{dist}(w, u)$. Доказательство:

Сложность алгоритма: $O(|V|) + O(|V|\cdot \text{insert}) + O(|V|\cdot \text{extract\_min}) + O(|E|\cdot \text{change\_priority})$

Если брать очередь на куче, то сложность $O((V+E)\log V)$. Если брать очередь на массиве, то сложность $O(V^2+E)$ (если граф простой, то $+E$ можно пренебречь).

Для неплотных $\left(E = O\left(\frac{V^2}{\log V}\right)\right)$ графов лучше использовать кучу, для плотных ($E \thicksim V^2$) — массив.

Tip: можно улучшить, если использовать $d$-ичную кучу. $d \approx \frac EV$.

(картинкА)

d-ичная куча

Если юзануть $d$-ичную кучу, то выйдет $n * d \cdot \log_d n$ — при extract_min. При insert'e: $n * \log_d n$, а decrease_key: $m * \log_d n$. Суммарная сложность: $O((n\cdot d + m) \log_d n)$.

Утверждается, что если $d \approx \frac{m}{n}$, то мы получим самое оптимальное решение.

  1. $\frac{m}{n} = O(1)$, т.е. $m \approx n$, т.е. $d = const \Rightarrow O((n+m) \log n) = O(n \log n)$

  2. Пусть граф полный. $m = \Omega(n^2)$, тогда $(d =) \frac{m}{n} = n \Rightarrow O(m) = O(n^2)$. Это то же что мы получали при реализации на массиве.

  3. $m = \Theta(n^{1+eps}), 0 < eps < 1$. Тогда $d = n^{eps}$. Итого $O(n\cdot d + m) \log_d n) = O((n\cdot n^{eps} + m) \cdot \log_{n^{eps}} n)$, расписав последний логарифм как $\frac{\log n}{eps \cdot \log n} = \frac{1}{eps}$ получаем $O(n^{1+eps}) = O(m)$.

Кратчайшие пути в графах с отрицательными весами

Дейкстра не работает (был пример),

* --5-->*
  \     \-6
   10----->*

Утверждение. Если в графе есть отрицательный цикл $\Rightarrow$ кратчайшие пути не определены.

Это достаточно очевидно утверждение. Мы бы тогда смогли построить путь из $s$ в $t$ отрицательной длины, и каждый раз бы пробовали его уменьшать (до $-\infty$).

Напоним про операцию релаксации:

  u --------> v
 d[u]         d[v]
if d[u] + w(u, v) < d[v]:
    d[v] = d[u] + w(u, v)

То есть если пройти по ребру $(u, v)$, то я смогу уменьшить суммарный вес пути. Поэтому до $v$ лучше идти через $u$.

Утверждение. Релаксация только "улучшает" пути (т.е. не увеличивает). Это видно из определения выше.

Утверждение: Релаксация корректна, $d[v] \geqslant dist(s, v)$. То есть релаксация не получит путь короче чем существует в графе.

Лемма: Пусть $\pi$ — кратчайший путь $(s, v_1, v_2, \ldots, v_k, t)$ из $s$ в $t$. Тогда если мы сделаем релаксации для всех рёбер вдоль пути $\Rightarrow$ $d[t] = dist(s, t)$.

Рассмотрим перое ребро из $s$: $(s, v_1)$. Если посчитаем релаксацию для этого ребра, то мы правильно посчитаем расстояние до $v_1$. По индукции и по следующему утверждению пути лемма доказана.

Утверждение: Если есть кратчайший путь $\pi$, то любой его подпуть тоже кратчайший. (Кстати, это место сломается, если мы ищем кратчайшие пустые пути). Если бы существовал не кратчайший подпуть, то там был бы цикл, т.е. и оригинальный путь не был бы кратчайшим.

Алгоритм Белмана-Форда

Как найти кратчайший путь? Можно просто релаксировать все рёбра случайным образом бесконечно много раз, и найдётся подпоследовательность релаксаций, которая найдёт мне кратчайший путь.

Теперь же хочется делать как можно меньше релаксаций, и чтобы там всё так же осталась подпоследовательность рёбер для нашего кратчайшего путя.

Прорелаксируем все рёбра в том порядке, в котором они пронумерованы: 1, 2, 3, ...; потом ещё раз: 1, 2, 3, ...; Так нужно делать $|V| - 1$ раз (длина самого длинного простого пути в графе). pi = 7, 5, 4, 13, 21, 2 — на каждой итерации релаксации, мы прорелаксируем сначала 7, потом 5, ..., последнюю вершину пути.

for v in V:
  d[v] = inf

d[s] = 0
for i = 1 to |V| - 1:
  for (u, v) in E:
    if d[v] > d[u] + w(u, v):
       d[v] = d[u] + w(u, v)
       prev[v] = u  # для восстановления пути

Утверждение: алгоритм корректный. Нужно доказать, что $\forall v: d[v] = dist(s, v)$. Посмотрим на кратчайший путь от $s$ до $v$, в нашей последовательности релаксаций был момент, когда мы релаксировали первое ребро, потом второе, ..., последнее, т.е. по предыдущей лемме получаем, что путь будет кратчайшим.

На циклах с отрицательными дугами не работает. Теперь зададимся вопросом: как определить наличие отрицательного цикла? Можно сделать следующее: запустим алгоритм Беллмана-Форда с $V$ итерациями. И на последнем шаге посмотрим, изменится ли значение — по идее, там не должно ничего измениться (релаксация может только улучшить расстояния, а мы уже посчитали все оптимальные расстояния на шаге $|V|-1$). Если всё же изменилось — вот он цикл отрицательной длины.

Утверждение: если отрицательных циклов нет $\Rightarrow$ на последней итерации $(= |V|)$, массив $d$ не изменится.

Утверждение: если есть отрицательный цикл, то массив $d$ обязательно (гарантированно) изменится.

Если делать не $V$ итераций а $\infty$, то релаксации будут бесконечно долго уменьшать $d$. Если на итерации $k$ ничего не изменилось, тогда ничего не изменится на итерации $k+1$ (стоп). То есть если у нас отрицательный цикл, то на каждой итерации будет что-то уменьшаться (иначе если что-то не изменится, то значит всё зафиксировано, т.е. не бесконечное количество шагов, т.е. нет цикла).

Из этого можно понять, что можно делать не ровно $V$ итераций, а даже меньше, но следить, изменилось ли что-нибудь.

Сложность $O(V \cdot E)$ (цикл по $V$ и цикл по $E$). Если выходить раньше, то будет $O(\texttt{radius}(G, s) \cdot E)$, где $radius(G, s)$ — длина самого длинного кратчайшего пути из $s$ до других вершин.

NB. Если граф ациклический, то можно сделать топологическую сортировку и пройти по ней в порядке топ. сортировки, вычислить за линейное время.

Расстояние от всех вершин до всех других

  1. Хотим считать расстояния от вершины $u$ до $v$ через меньшие подзадачи. Определим $D(u, v, k)$ — кратчайший путь от $u$ до $v$, который проходит через вершины $\{ 1, 2, \ldots, k\}$ (необязательно все, но имеется в виду принадлежность множеству).

  2. $D(u, v, 0) = w(u, v)$, если $(u, v) \in E$, иначе $\infty$. Если представить куб, то его первый срез — это просто веса рёбер, то есть матрица смежности.
    кратчайший путь от $u$ до $v$ проходит через $k$, либо не проходит. $D(u, v, k) = \min \{ D(u, v, k-1), D(u, k, k-1) + D(k, v, k-1) \}$ где, вершина $k-1$ находится между $k$ и $v$ во втором случае.

  3. Двигаемся вдоль координаты $k$.

Алгоритм Флойда-Уоршала:

for u = 1 to n:
  for v = 1 to n:
    d[u, v, 0] = w(u, v)  #inf, если ребра нет

for k = 1 to n:
  for u = 1 to n:
    for v = 1 to n:
      d[u, v, k] = d[u, v, k - 1]
      if d[u, v, k] > d[u, v, k-1] + d[k, v, k-1]:
        d[u, v, k] = d[u, v, k-1] + d[k, v, k-1]

Время $O(V^3)$. Память $O(V^3)$, но мы можем хранить только предыдущий слой: $O(V^2)$.

for k = 1 to n:
  for u = 1 to n:
    for v = 1 to n:
      if d[u, v] > d[u, v] + d[k, v]:
        d[u, v]  = d[u, v] + d[k, v]

Можно хранить $prev[u, v]$, которая хранит вершину перед $v$. И при присваивании меняем $prev$.

# init
if (u, w) in E:
  prev[u, v] = u

# loop
if d[u, v] > d[u, v] + d[k, v]:
  d[u, v]  = d[u, v] + d[k, v]
  prev[u, v] = prev[k, v]

Отрицательные рёбра норм, мы нигде не пользовались фактом, что их нет.

Отрицательные циклы можно детектировать, если на диагонали (кроме нулей), появятся отрицательные числа — то есть мы из вершины прошли в саму себя же.