Strongly connected components
Утверждения:
- Если рассматривать любой ациклический граф, то в нем есть сток и исток
- Если граф G ациклический, то тогда и только тогда GR (граф с перевернутыми стрелками) тоже ациклический (и наоборот, если есть цикл в одном графе, то есть и в другом)
- GR можно построить за O(|V|+|E|) по G, заданному списком смежности.
Компонента сильной связности — это максимальное подвключение подмножества V такое, что ∀u,v∈V существует путь u→v→u.
Метаграф — граф, в котором все компоненты сильной связности заменены на единичные вершины (с сохранением ребер между компонентами). Если есть несколько ребер между двумя компонентами, то, в зависимости от задачи, можно либо их склеить в одно ребро, либо оставить все.
Утверждение: — метаграф ациклический. Доказательство: — если бы в нем был цикл, то несколько компонент сильной связности склеились бы в одну.
Утверждение: — есть две вершины U и V (в метаграфе), которые задают две компоненты сильной связности, (U→V)∈H (H — множество ребер метаграфа). max (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]
Отрицательные рёбра норм, мы нигде не пользовались фактом, что их нет.
Отрицательные циклы можно детектировать, если на диагонали (кроме нулей), появятся отрицательные числа — то есть мы из вершины прошли в саму себя же.