Strongly connected components
Утверждения:
- Если рассматривать любой ациклический граф, то в нем есть сток и исток
- Если граф $G$ ациклический, то тогда и только тогда $G^R$ (граф с перевернутыми стрелками) тоже ациклический (и наоборот, если есть цикл в одном графе, то есть и в другом)
- $G^R$ можно построить за $O(|V|+|E|)$ по $G$, заданному списком смежности.
Компонента сильной связности — это максимальное подвключение подмножества $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 на это ребро). Минусы:
- требует целых/дробных весов
- не работает на иррациональных числах (на это пофиг, т.к. иррациональность не представима на машине Тьюринга)
- требует много памяти на ребрах с большим весом
Алгоритм Дейкстры
Работает для графов с неотрицательными ребрами.
Для реализации нужна очередь с приоритетами с операциями:
extract_min()
change_priority(value, key)
insert(value, priority)
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)$. Доказательство:
- Для вершин из другой компоненты связности корректность очевидна.
-
Ведем три множества:
- $S$ — вершины, которые мы уже обработали (которые вытащили из очереди)
- $R$ — вершины, которые сейчас в очереди
- $T$ — вершины, с расстоянием равным $+\infty$ Вершины в $R$ — соседи вершин из $S$. Утверждение: если $v$ переходит из $R$ в $S \Rightarrow \ d[v] = \text{dist}(s, v)$ Доказательство: пусть $v$ — вершина в очереди с самым маленьким приоритетом. И.п.: для всех вершин в $S$ расстояние вычисленно корректно. Докажем от противного: пусть $\Pi = (s, v1, \ ... \, v_k, v)$ — более короткий путь $s\rightarrow v$. Рассмотрим первую вершину этого пути, которая лежит в $R$. Расстояние от нее до $s$ больше, чем предпологаемое расстояние до $v$, которое лежит в очереди. Противоречие.
Сложность алгоритма: $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}$, то мы получим самое оптимальное решение.
-
$\frac{m}{n} = O(1)$, т.е. $m \approx n$, т.е. $d = const \Rightarrow O((n+m) \log n) = O(n \log n)$
-
Пусть граф полный. $m = \Omega(n^2)$, тогда $(d =) \frac{m}{n} = n \Rightarrow O(m) = O(n^2)$. Это то же что мы получали при реализации на массиве.
-
$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. Если граф ациклический, то можно сделать топологическую сортировку и пройти по ней в порядке топ. сортировки, вычислить за линейное время.
Расстояние от всех вершин до всех других
-
Хотим считать расстояния от вершины $u$ до $v$ через меньшие подзадачи. Определим $D(u, v, k)$ — кратчайший путь от $u$ до $v$, который проходит через вершины $\{ 1, 2, \ldots, k\}$ (необязательно все, но имеется в виду принадлежность множеству).
-
$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$ во втором случае. -
Двигаемся вдоль координаты $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]
Отрицательные рёбра норм, мы нигде не пользовались фактом, что их нет.
Отрицательные циклы можно детектировать, если на диагонали (кроме нулей), появятся отрицательные числа — то есть мы из вершины прошли в саму себя же.