Ctrl+Enter

Так как листок пишется с нуля, то в нём хватает опечаток, неточностей и ошибок. Если вы заметили опечатку или ошибку, выделите её, нажмите Ctrl+Enter и опишите проблему. Желательно также написать ещё и её решение.
Точно также можно поступить, если какой-то кусок текста совсем непонятен. А если вы можете предложить замену некоторой части на гораздо более понятный текст — то будет просто замечательно!

PS. Перед тем, как отправить ошибку, обновите страницу. Возможно, ошибка уже исправлена.
PSS. Не нужно тестировать эту функцию просто так, она вроде работает. Но каждое замечание я буду вынужден читать :)

Напоминание

graph

Граф задаётся множеством вершин V и множеством рёбер E, соединяющих пары вершин. (Английская терминология: вершина называется vertex или node, а ребро — edge.)

В примере с картой V = {1, 2, 3, ...‌, 13}, а рёбра из E соединяют страны, граничащие друг с другом (в частности, E содержит рёбра {1, 2}, {9, 11} и {7, 13}). Отношение «быть соседом» симметрично, так что ребро из x в y возникает одновременно с ребром из y в x. Такие рёбра называются неориентированными (undirected), а соответствующий граф — неориентированным графом (undirected graph).

Бывают и несимметричные отношения, и их изображают ориентированными рёбрами (directed edges). В ориентированном графе наличие ребра из x в y не гарантирует наличие ребра из y в x.

Если явно не указано обратное, то рассматривают только графы без петель (loop) и без кратных рёбер (multiple edge). Такие графы называются простыми (simple).

Самые простые графы — это деревья. Во многих задачах, связанных с графами, используется обходы графа, в результате которых в графе выделяются деревья, состоящие из некоторых вершин графа и некоторых его рёбер. У этих деревьев отмечена вершина, которая называется его корнем (root). Дерево с отмеченной вершиной — корнем — называется корневым деревом (rooted tree).

В корневых деревьях чётко различаются путь к корню и путь от корня. Сосед вершины на пути к корню называется её родителем (parent). Соседи вершины на пути от корня называются её детьми или сыновьими вершинами (child, children). Все вершины на пути из вершины к корню, кроме исходной, называются её предками (ancestor). Все вершины на пути из корня называются потомками (descendant). Вершины, у которых общий родитель, называются братьями (sibling). И наконец вершины, у которых нет потомков, называются листьями (leaf, leaves, terminal vertex).

Представление графа

Рассмотрим граф c \(n = |V|\) вершинами \(v_1,\ldots‌,v_n\). Его матрицей смежности (adjacency matrix) называется \((n\times n)\)-матрица a, в которой \[ a_{ij} = \begin{cases} 1,&\text{если есть ребро из $v_i$ в $v_j$}\\ 0 &\text{иначе} \end{cases} \] Матрица смежности неориентированного графа, таким образом, симметрична (\(a_{ij} = a_{ji}\)). В таком представлении мы можем за время O(1) проверить, соединены ли данные вершины ребром (посмотрев на один элемент массива). В то же время хранение матрицы требует памяти O(n2), что во многих случаях неэкономно. Альтернативное представление — список смежности (adjacency list). Требуемая при этом память пропорциональна размеру графа (сумме числа вершин и числа рёбер). Элементами списка смежности являются списки, по одному для каждой вершины графа. Для вершины u в таком списке хранятся вершины, в которые ведут рёбра из u, то есть вершины v, для которых \((u, v) \in E\). Для ориентированного графа каждое ребро входит только в один из этих списков (для начальной вершины), а для неориентированного в два (для двух концов ребра). Список смежности, таким образом, требует памяти \(O(|V|+ |E|)\). Проверка наличия ребра \((u, v)\) теперь требует просмотра списка вершины u (что может быть больше O(1) шагов, если вершины хранятся именно в списке, а не в множестве или словаре). Зато в этом представлении легко просмотреть всех соседей заданной вершины (что часто бывает необходимо). Список смежности для неориентированного графа симметричен (если u содержится в списке для v, то и v содержится в списке для u).

Матрица смежности или список смежности?

Что лучше? Ответ зависит от отношения между числом вершин \(|V|\) и числом рёбер \(|E|\). Заметим, что \(|E|\) может быть довольно малым — порядка \(|V|\) (если \(|E|\) сильно меньше \(|V|\), то граф уже вырожденный — в частности, содержит изолированные вершины). Или же довольно большим — порядка \(|V|^2\) (если граф содержит все возможные рёбра). Графы с большим числом рёбер называют плотными (dense), с малым — разреженными (sparse).
Выбирая алгоритм, полезно понимать, с какими графами ему в основном придётся иметь дело. Важно это и при хранении: если хранить веб-граф, в котором больше восьми миллиардов вершин, в виде матрицы смежности, то занимать она будет миллионы терабайтов, что сравнимо с общей ёмкостью всех жёстких дисков в мире. И дальше, скорее всего, будет только хуже (матрица может расти быстрее, чем производство дисков). А вот хранить граф Интернета в виде списка смежности вполне разумно, поскольку хранить нужно несколько десятков миллиардов гиперссылок, каждая из которых будет занимать в списке всего несколько байтов, и такого размера диск поместится в карман. Такая разница происходит из-за того, что граф Интернета очень разрежен: страница содержит в среднем пару десятков ссылок на другие страницы — из нескольких миллиардов возможных.

Напоминание про множества и словари

Про множества

Задание множеств

A = set()  # Пустое множество
A = {1, 2, 'hello'}  # Явное перечисление элементов
A = set('hello')  # Множество букв в строке
B = [1, 2, 1, 2]; A = set(B)  # Множество из списка или любого итерируемого объекта
A = {i**2 for i in range(10)}  # Генератор множеств

Работа с элементами множеств

C = {1, 2, 'hello'}
for elem in C:  # Перебираем все элементы множества
    print(elem)
sorted(C)  # Список из отсортированных элементов множества
1 in C  # Проверка принадлежности
A.add(3)  # Добавление элемента
A.remove(3)  # Удаление элемента, который есть в множестве
A.discard(4)  # Удаление элемента, которого может не быть в множестве
A.pop()  # Извлечение случайного элемента из множества с удалением его

Операции с множествами

len(A)  # Количество элементов в множестве
A | B  # Возвращает множество, являющееся объединением множеств A и B.
A |= B  # Добавляет в множество A все элементы из множества B.
A & B  # Возвращает множество, являющееся пересечением множеств A и B.
A &= B  # Оставляет в множестве A только те элементы, которые есть в множестве B.
A - B  # Возвращает разность множеств A и B (A, но не B).
A -= B  # Удаляет из множества A все элементы, входящие в B.
A ^ B  # Возвращает симметрическую  разность множеств A и B.
A ^= B  # Записывает в A симметрическую разность множеств A и B.
A <= B  # Возвращает true, если A является подмножеством B.
A >= B  # Возвращает true, если B является подмножеством A.
A < B  # Эквивалентно A <= B and A != B
A > B  # Эквивалентно A >= B and A != B
Про словари

Задание словарей

D = {}  # Пустой словарь
D = {1: 'a', 2: 'b'}  # Явное перечисление
D = dict([(1, 'a'), (2, 'b')])  # Словарь из списка пар элементов
D = dict(zip([1, 2], ['a', 'b']))  # Словарь из итерируемого объекта, возвращающего пары элементов (ключ-значение)
D = {i: chr(i + ord('a')) for i in range(1, 3)}  # Генератор словарей

Работа с элементами словаря

len(D)  # Количество элементов в словаре
D[key]  # Поиск по ключу, который есть в словаре
key in D  # Проверка принадлежности словарю
D[key] = value  # Установка или изменение значения
del D[key]  # Удаление ключа, который есть в словаре
value = D.pop(key)  # Удаления ключа вместе с возвращением значения
value = D.pop(key, no_key_value)  # Удаления ключа вместе с возвращением значения. Если ключа нет, то no_key_value
key, value = D.popitem()  # Извлечение из словаря пары (ключ, значение) с удалением ключа
D.get(key, no_key_value)  # Значение по ключу, no_key_value, если ключа нет
D[key] = D.get(key, 0) + 1  # Самая простая реализация счётчика
for key in D:  # Перебираем все ключи
    print(key, D[key])
for key, value in D.items():  # Перебираем все пары (ключ, значение)
    print(key, value)
for value in D.values():  # Перебор всех значений
    print(value)
sorted(D)  # Отсортированный список ключей
sorted(D.values())  # Отсортированный список значений
sorted(D.items())  # Отсортированный по ключу список пар (ключ, значение)
sorted(D.items(), key=lambda x: x[1])  # Отсортированный по значению список пар (ключ, значение)

Список смежности и матрица смежности: туда и обратно

01. От матрицы смежности к списку ребер, неориентированный вариант

Простой неориентированный граф задан матрицей смежности, выведите его представление в виде списка ребер.

Входные данные включают число n — количество вершин в графе, а затем n строк по n чисел, каждое из которых равно 0 или 1, — его матрицу смежности. Выведите список ребер заданного графа (в любом порядке).

ВВОД:
5
0 0 1 0 0 
0 0 1 0 1 
1 1 0 0 0 
0 0 0 0 0 
0 1 0 0 0 
ВЫВОД
1 3
2 3
2 5

02. От списка ребер к матрице смежности, неориентированный вариант

Простой неориентированный граф задан списком ребер, выведите его представление в виде матрицы смежности.

Входные данные включают число n — количество вершин в графе, и число m — количество рёбер. Затем следует m пар чисел — ребра графа. Выведите матрицу смежности заданного графа.

ВВОД:
5 3
1 3
2 3
2 5
ВЫВОД
0 0 1 0 0 
0 0 1 0 1 
1 1 0 0 0 
0 0 0 0 0 
0 1 0 0 0 

03. От матрицы смежности к списку ребер, ориентированный вариант

Ориентированный граф задан матрицей смежности, выведите его представление в виде списка ребер.

Входные данные включают число n — количество вершин в графе, а затем n строк по n чисел, каждое из которых равно 0 или 1, — его матрицу смежности. Выведите список ребер заданного графа (в любом порядке).

ВВОД:
5
0 0 0 0 0 
0 0 0 0 1 
1 1 0 0 0 
0 0 0 0 0 
0 0 0 0 0 
ВЫВОД
2 5
3 1
3 2

04. От списка ребер к матрице смежности, ориентированный вариант

Простой ориентированный граф задан списком ребер, выведите его представление в виде матрицы смежности.

Входные данные включают число n — количество вершин в графе, и число m — количество рёбер. Затем следует m пар чисел — ребра графа. Выведите матрицу смежности заданного графа.

ВВОД:
5 3
1 3
2 3
5 2
ВЫВОД
0 0 1 0 0 
0 0 1 0 0 
0 0 0 0 0 
0 0 0 0 0 
0 1 0 0 0 

Метки времени при обходе в глубину

time_marks Метки времени — приём, который лежит в основе многих применений поиска в глубину.

Обычный алгоритм обхода в глубину описывается рекурсивно: сначала выводим рассматриваемую вершину, а затем для каждого сына выполняем обход в глубину.

Метки времени — это натуральные числа от \(1\) до \(2|V|\). Каждая вершина при обходе в глубину получает две метки: время начала обработки и время окончания, причём все метки всех вершин различны.

Заведём счётчик времени. Изначально счётчик равен нулю. Каждый раз, когда мы переходим от одной вершины к другой мы прибавляем к счётчику единицу. Время начала обработки — это значение счётчика в тот момент, когда мы «выводим рассматриваемую вершину». Время окончания — это значение счётчика после того, как будет выполнен обход в глубину для всех сыновьих вершин. Каждый раз, когда мы начинаем обход для какой-либо вершины, мы прибавляем 1 к счётчику. Аналогично мы прибавляем 1 к счётчику после того, как закончили обход для данной вершины.

На картинке справа приведён обход в глубину с отметками времени. Дополнительно простановку меток времени можно представлять себе так: нарисуем дерево на плоскости, после чего обведём его пунктиром начиная с корня. Каждый раз, когда наш пунктир проходит слева или справа от вершины, мы записываем число — порядковый номер встречи с вершиной. Числа, записанные слева от каждой вершины будут временем начала обработки, числа справа — временем окончания.

Заметим сначала довольно банальное свойство меток времени: если отсортировать все вершины по возрастанию времени начала обработки или по убыванию времени окончания обработки, то получившийся порядок будет соответствовать некоторому обходу в глубину.

Следующее свойство — времена начала и окончания образуют правильную скобочную структуру. Если мы в последовательности чисел от \(1\) до \(2|V|\) заменим числа, соответствующие меткам начала обработки на открывающую скобку, а числа, соответствующие меткам окончания обработки на закрывающую, то в результате получится правильная скобочная структура. Так в примере выше получается \[ \mathbf{(\ (\ (\ )\ (\ )\ (\ )\ )\ (\ (\ (\ )\ )\ (\ )\ )\ )} \]

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

Когда мы будем работать с более сложными графами, то увидим ещё несколько применений меткам времени.

05. Метки времени при обходе в глубину дерева


На вход подаётся корневое дерево в формате задач 01 и 02 из предыдущего листка. Выполните обход дерева в глубину начиная с корня и вычислите метки времени для вашего обхода. Выведите вершины в лексикографическом порядке, указав для каждой вершины время начала и время окончания обработки.
(Конкретные времена зависят от обхода и могут не совпадать с временами в примере)

Для решения могут пригодиться глобальные переменные. Напоминание здесь.

ВВОД:
9
B A
C B
D B
E B
F A
G F
H G
I F
ВЫВОД
A 1 18
B 2 9
C 3 4
D 5 6
E 7 8
F 10 17
G 11 14
H 12 13
I 15 16

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Для решения нужно "усилить" самый обычный рекурсивный обход в глубину. Нужно создать глобальные переменные time=0, time_in={} и time_out={}. В начале функции нужно прибавлять к времени 1 и заносить время в словарь time_in, после обхода всех сыновьих вершин нужно прибавлять к времени 1 и заносить время в словарь time_out.

Несложно модифицировать и нерекурсивный алгоритм на основе стека. Идея будет следующей: после того, как мы сняли вершину со стека, нужно проверить, если ли она в словаре time_in. Если есть, то выбросить её и идти дальше. А если нет, то добавить метку времени, ещё раз добавить её в стек и начать обходить её сыновьи вершины. После того, как все они будут обработаны, мы снова вернёмся к этой вершине и проставим время окончания обработки.

06. Предки и потомки — 2

Даны два элемента в дереве. Определите, является ли один из них потомком другого.

Программа получает на вход описание дерева. Далее идёт натуральное число M — количество запросов. После этого идёт M строк, содержащие имена двух элементов дерева. Для каждого такого запроса выведите одно из трех чисел: 1, если первый элемент является предком второго (или они совпадают), 2, если второй является предком первого или 0, если ни один из них не является предком другого. Число M будет очень велико, поэтому каждая такая проверка должна требовать константу времени. Для этого можно использовать метки времени из предыдущей задачи.

ВВОД:
4
Alice Jake
Caitlin Jake
Grace Alice
3
Alice Jake
Jake Alice
Alice Caitlin
ВЫВОД
2
1
0

Как хранить графы в программе

В графе в отличие от корневого дерева нет корня и нет разницы между предками и потомками. Все рёбра равнозначны. Поэтому удобно хранить граф в виде словаря, ключами которого являются все вершины, а значениями — множество вершин, соединённых с данной. Если граф неориентированный, то каждое ребро (v, w) "хранится" сразу в двух множествах.

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

07. digraph_from_input()

Directed_acyclic_graph.png

Программа получает на вход число рёбер в ориентированном графе N. Далее следует N строк, задающие рёбра графа. Каждая строка имеет вид откуда_ребро куда_ребро.

Программа должна сформировать словарь digraph смежности ориентированного графа и вывести его при помощи функции pprint из модуля pprint.

Реализуйте функцию digraph_from_input(), которая считывает граф в таком формате из стандартного ввода и возвращает словарь digraph.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
{'A': {'B'},
 'B': {'C', 'E', 'D'},
 'C': {'E'},
 'D': {'E'},
 'E': {'F'},
 'F': set(),
 'G': {'D'}}

08. graph_from_input()

Indirected_acyclic_graph.png

Программа получает на вход число рёбер в неориентированном графе N. Далее следует N строк, задающие рёбра графа. Каждая строка имеет вид откуда_ребро куда_ребро.

Программа должна сформировать словарь graph смежности неориентированного графа и вывести его при помощи функции pprint из модуля pprint.

Реализуйте функцию graph_from_input(), которая считывает граф в таком формате из стандартного ввода и возвращает словарь graph.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
{'A': {'B'},
 'B': {'A', 'C', 'E', 'D'},
 'C': {'B', 'E'},
 'D': {'B', 'E', 'G'},
 'E': {'C', 'B', 'D', 'F'},
 'F': {'E'},
 'G': {'D'}}

09. Степень вершины

Indirected_acyclic_graph.png

Дан неориентированный граф.

Выведите степень каждой вершины. Вершины должны выводиться в лексикографическом порядке.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
A 1
B 4
C 2
D 3
E 4
F 1
G 1

10. Эйлеров цикл в связном графе

Indirected_acyclic_graph.png

Дан связный неориентированный граф.

Выведите YES, если в нём есть эйлеров цикл, и NO иначе.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
NO
           
ВВОД:
3
A B
B C
C A
ВЫВОД
YES

11. Из орграфа в графы

Directed_acyclic_graph.png

Ориентированный граф кратко называется орграфом. Для краткости далее под графом всегда будет пониматься только неориентированный граф.

Из орграфа всегда можно сделать граф, убрав стрелочки с вершин. На вход подаётся строка, в которой записан словарь digraph. Его можно записать в переменную при помощи кода

digraph = eval(input())
Сделайте из этого орграфа неориентированный граф и выведите его при помощи pprint.

ВВОД:
{'A': {'B'}, 'B': {'C', 'D', 'E'}, 'C': {'E'}, 'D': {'E'}, 'E': {'F'}, 'F': set(), 'G': {'D'}}

ВЫВОД
{'A': {'B'},
 'B': {'A', 'C', 'E', 'D'},
 'C': {'B', 'E'},
 'D': {'B', 'E', 'G'},
 'E': {'C', 'B', 'D', 'F'},
 'F': {'E'},
 'G': {'D'}}

12. Входящая и исходящая степень вершины

Directed_acyclic_graph.png

Дан ориентированный граф.

Выведите входящую (количество входящих рёбер, indegree) и исходящую (количество исходящих рёбер, outdegree) степень каждой вершины. Вершины должны выводиться в лексикографическом порядке.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
A 0 1
B 1 3
C 1 1
D 2 1
E 3 1
F 1 0
G 0 1

13. Квадрат орграфа

Directed_acyclic_graph.png

Дан ориентированный граф.

Квадратом графа называется граф с тем же множеством вершин, что и исходный, но в котором из вершины v проведено ребро в w, если в исходном графе из вершины v можно попасть в w не более, чем за 2 шага.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
 
{'A': {'C', 'B', 'E', 'D'},
 'B': {'C', 'E', 'D', 'F'},
 'C': {'E', 'F'},
 'D': {'E', 'F'},
 'E': {'F'},
 'F': set(),
 'G': {'E', 'D'}}

14. Транспонирование орграфа

Directed_acyclic_graph.png

Дан ориентированный граф.

Транспонированным графом называется граф с тем же множеством вершин, что и исходный, но в котором из вершины v проведено ребро в w тогда и только тогда, когда в исходном графе есть ребро из w в v.

На вход подаётся строка, в которой записан словарь digraph. Транспонируйте этот орграф и выведите его при помощи pprint.

ВВОД:
{'A': {'B'}, 'B': {'C', 'D', 'E'}, 'C': {'E'}, 'D': {'E'}, 'E': {'F'}, 'F': set(), 'G': {'D'}}

ВЫВОД
{'A': set(),
 'B': {'A'},
 'C': {'B'},
 'D': {'B', 'G'},
 'E': {'C', 'B', 'D'},
 'F': {'E'},
 'G': set()}

15. Обратный граф

Indirected_acyclic_graph.png

Дан неориентированный граф.

Дополнением или обратным графом называется граф с тем же множеством вершин, что и исходный, но в котором из вершины v проведено ребро в w тогда и только тогда, когда в исходном графе нет ребра из v в w.

На вход подаётся граф в формате из задачи 08. Создайте обратный граф и выведите его при помощи pprint.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
{'A': {'C', 'E', 'D', 'G', 'F'},
 'B': {'G', 'F'},
 'C': {'A', 'D', 'G', 'F'},
 'D': {'A', 'C', 'F'},
 'E': {'A', 'G'},
 'F': {'A', 'C', 'B', 'D', 'G'},
 'G': {'A', 'C', 'B', 'E', 'F'}}

16. Обход в ширину

Directed_acyclic_graph.png

Дан ориентированный граф и одна из его вершин.

Выполните обход графа в ширину начиная с этой вершины.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
ВЫВОД
A
B
C
E
D
F

Подсказка (а также прочитать после решения)
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Здесь нужно делать всё также, как и для деревьев, только дополнительно нужно хранить множество посещённых вершин и не ходить в них повторно.

Есть два самых простых способа делать такой обход: при помощи слоёв или при помощи очереди. В разных случаях конкретный способ может быть удобнее благодаря некоторым бонусам.

Решение при помощи слоёв: Будем хранить два множества: множество элементов, которые необходимо обойти на текущем уровне дерева, а также множество встреченных элементов следующего уровня.
Сначала добавляем в первое множество начальную вершину, а второе оставляем пустым. Далее до тех пор, пока первое множество не иссякнет выполняем процедуру обхода: для каждого элемента первого множества выводим его, после чего добавляем всех его потомков во второе множество. После того, как все элементы первого множества будут обработаны, назначаем второе множество первым, а пустое множество — новым вторым.
Решение при помощи слоёв удобно для вычисления минимальных расстояний, количества кратчайших путей или просто путей и подобных задач.

Решение при помощи очереди. Воспользуемся деком из модуля collections, он позволяет эффективно добавлять элементы слева списка, а забирать — справа. Такая структура данных называется очередью. Добавим начальную вершину в очередь. Множество посещённых вершин пока пусто. Пока очередь не пуста будем выполнять простое действие: снимаем вершину с очереди, выводим её, добавляем в множество посещённых. И добавляем всех её ещё не посещённых соседей в очередь.
Решение при помощи очереди хорошо обобщается на графы с разной стоимостью рёбер. Например, поиск коротких путей в 0-1 графах (см. задачу ниже) делается из обычного обхода буквально в пару строчек.

Пример операций с очередью:

from collections import deque
queue = deque([start_node])
visit = queue.pop()
queue.appendleft(next_node)

17. Обход в глубину

Directed_acyclic_graph.png

Дан ориентированный граф и одна из его вершин.

Выполните обход графа в глубину начиная с этой вершины.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
ВЫВОД
A
B
С
E
F
D

Подсказка (а также прочитать после решения)
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Здесь нужно делать всё также, как и для деревьев, только дополнительно нужно хранить множество посещённых вершин и не ходить в них повторно.

Есть три самых распространённых способа выполнения обхода в глубину. Как обычно в разных случаях конкретный способ может быть удобнее благодаря некоторым бонусам.

Решение при помощи рекурсии: Это самое простое с точки зрения реализации решение. Стартуем с начальной вершины. Проверяем, посещена ли текущая вершина. Если посещена, то заканчиваем обход. Если ещё нет, по посещаем (выводим и добавляем в множество посещённых вершин), после чего рекурсивно применяем процедуру ко всем ещё не посещённым соседям.
Само решение достаточно простое, и его легко модифицировать так, чтобы получать отметки времени входа и выхода. В решении нужно не забыть увеличить максимальную глубину рекурсии.

Решение при помощи стека. Добавляем начальную вершину в стек (в Python'е — в обычный список при помощи append), а также в множество посещённых вершин. Далее работаем в цикле пока стек не пуст: снимаем вершину со стека (в Python'е — при помощи pop). Проверяем, посещена ли она. Если ещё нет, по посещаем (выводим и добавляем в множество посещённых вершин), после чего добавляем в стек всех ещё не посещённых соседей.
Решение удобно своей краткостью, простотой и эффективностью, однако его достаточно дополнять фичами.

Решение при помощи стека и покраски вершин. Будем обходить граф и попутно красить вершины. Цвет вершины будем хранить в словаре. Изначально все вершины белые — это значит, мы с этими вершинами ещё ни разу не сталкивались. Будем использовать стек, в котором в каждый момент времени будет храниться путь из начальной вершины до текущей, а также множество ещё необработанных соседей каждой вершины в стеке.
Кладём в стек пару из начальной вершины и множества её соседей, после чего красим её в серый цвет. Серый цвет означает, что мы уже встречали эту вершину, но ещё не обошли в глубину всех её соседей.
Дальше пока стек не пуст выполняем: Смотрим на вершину стека. Если в множество соседей для обработки пусто, то красим вершину в чёрный цвет (что означает, что все соседи данной вершины уже пройдены), после чего удаляем её (вместе с уже пустым множеством её необработанных соседей) из стека. Если же множество ещё непусто, то изымаем из него очередную вершину. Если она белая, то красим её в серый и добавляем в стек её и множество её соседей. Иначе игнорируем её.
Такое решение удобно и для вычисления отметок весов (время входа — момент покраски в серый, времы выхода — в чёрный), для классификации рёбер на древесные, прямые, обратные и перекрёстные. В таком решении легко вывести путь из корня до текущей вершины (нужно просто пройти по стеку). В таком решении удобно искать циклы: если при обработке множества необработанных соседей мы встретили серую вершину, то тем самым нашли цикл, так как серые вершины — это в точности вершины в нашем стеке. А ещё с помощью такого решения удобно делать топологическую сортировку.

18. Достижимость

Directed_acyclic_graph.png

Дан ориентированный граф и несколько пар его вершин.

Выведите 'YES', если существует путь из первой вершины пары во вторую и 'NO' иначе.

Сложность проверки для каждой пары вершин должна быть не больше \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
3
A B
A F
A G
ВЫВОД
YES
YES
NO

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

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

19. Минимальное расстояние до вершины

Directed_acyclic_graph.png

Дан ориентированный граф и отмечена одна из его вершин.

Для каждой вершины графа выведите длину минимального пути из отмеченной. Если такого пути нет, то выведите -1. Вершины должны выводиться в лексикографическом порядке.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Здесь удобно сделать обход в ширину по слоям (см. подсказку к обходу в ширину). При этом мы для каждого слоя будем в словаре сохранять его расстояние до начальной вершины.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
ВЫВОД
A 0
B 1
C 2
D 2
E 2
F 3
G -1

20. Количество компонент связности

Indirected_acyclic_graph.png

Дан неориентированный граф.

Выведите количество его компонент связности.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
1
           
ВВОД:
3
A B
C D
E F
ВЫВОД
3

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Идея снова очень проста: берём вершину и делаем из неё обход в ширину или в глубину. Все посещённые вершины дают компоненту связности. Дальше нужно брать ещё непосещённую вершину и повторять до тех пор, пока все вершины будут посещены.

21. Дерево поиска в ширину

Directed_acyclic_graph.png

Дан ориентированный граф и одна из его вершин.

Выполним обход в ширину начиная с этой вершины. Рассмотрим множество рёбер, после прохода по которым мы попадали в новую вершину. Они образуют корневое дерево с корнем в исходной вершине. Выведите словарь parents этого корневого дерева.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
ВЫВОД
{'A': None, 'B': 'A', 'C': 'B', 'D': 'B', 'E': 'B', 'F': 'E'}

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Достаточно легко модифицируется любой вариант обхода в ширину. Каждый раз, когда мы "посещаем" вершину во время обхода в ширину, нужно добавить её предка в словарь. Отдельно нужно добавить запись для корня (с ссылкой на Node).

22. Самые короткие пути

Directed_acyclic_graph.png

Дан ориентированный граф и отмечена одна из его вершин V.

Далее перечисляются несколько вершин этого графа. Для каждой вершины W выведите какой-нибудь из самых коротких путей из вершины V в вершину W, и -1 если такого пути нет.

Сложность подготовки должна быть \(O(|V| + |E|)\). Сложность нахождения какой-нибудь из самых коротких путей после подготовки должна быть порядка длины этого пути.

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
3
B
F
G
ВЫВОД
A B
A B E F
-1

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

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

23. Дерево поиска в глубину

Directed_acyclic_graph.png

Дан ориентированный граф и одна из его вершин.

Выполним обход в глубину начиная с этой вершины. Рассмотрим множество рёбер, после прохода по которым мы попадали в новую вершину. Они образуют корневое дерево с корнем в исходной вершине. Выведите словарь parents этого корневого дерева.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
A
ВЫВОД
{'A': None, 'B': 'A', 'C': 'B', 'D': 'B', 'E': 'С', 'F': 'E'}

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

Достаточно легко модифицируется любой вариант обхода в глубину. Каждый раз, когда мы "посещаем" вершину во время обхода в глубину, нужно добавить её предка в словарь. Отдельно нужно добавить запись для корня (с ссылкой на Node).

24. Цикл в графе

Indirected_acyclic_graph.png

Дан неориентированный граф.

Если в графе есть цикл, то выведите 'YES' и вершины любого цикла. В противном случае выведите 'NO'.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
YES
B C E D
           
ВВОД:
3
A B
B C
C D
ВЫВОД
NO

Подсказка
Да, мне кажется, что я уже достаточно думал над этой задачей
Чес-слово! :)

И эта задача решается при помощи обхода. Выполняем обход в глубину или в ширину, сохраняя при этом дерево обхода (см. соответствующие задачи). Как только очередное ребро смотрит в посещённую вершину — мы получили цикл. Проще всего его получить пройдя от этих вершин до их наименьшего общего предка (или прямо до корня).

25. Цикл в орграфе

Directed_acyclic_graph.png

Дан ориентированный граф.

Если в графе есть цикл, то выведите 'YES' и вершины любого цикла. В противном случае выведите 'NO'.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
NO
           
ВВОД:
3
A B
B C
C A
ВЫВОД
YES
A B C

26. Топологическая сортировка

Directed_acyclic_graph.png

Дан ориентированный граф без циклов. Необходимо вывести всего его вершины в таком порядке, чтобы из любой вершины с б́ольшим номером не существовало пути ни в какую вершину с м́еньшим номером.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\) или \(O(\log(|V|)\cdot(|V| + |E|))\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
G A B C D E F

27. Количество путей

Indirected_acyclic_graph.png

Дан ациклический ориентированный граф и несколько пар его вершин. Определите количество простых путей (то есть путей, все рёбра которого попарно различны) и одной вершины в другую.

Сложность получившегося алгоритма не считая сложности арифметических операций должна быть \(O(|V| + |E|)\). (Например, количество путей в полном графе будет порядка \(n!\), для больших \(n\) арифметические операции с такими числами занимают существенное время)

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
3
B E
B A
G F
ВЫВОД
3
0
1

28. Кратчайший путь в 0-1-графе

Имеется схема городского транспорта (неориентированный граф), в котором участвую автобусы и троллейбусы. Проезд на автобусе стоит 1$, проезд на троллейбусе бесплатен. Необходимо найти какой-нибудь самый дешёвый способ добраться от одной остановки до другой. Если такого пути нет, то выведите -1.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
7
A B 1
B C 0
C D 1
A F 0
F E 1
E D 0
L M 0
3
A D
B C
A L
ВЫВОД
A F E D
B C
-1

29. Кратчайший цикл

Дан ориентированный граф. Необходимо вывести любой из кратчайших циклов. Если ни одного цикла нет, то выведите -1.

Сложность получившегося алгоритма должна быть \(O(|V|\cdot(|V| + |E|))\).

ВВОД:
7
A B
B C
C D
D E
B F
F E
E A
ВЫВОД
A B F E A

30. Кратчайший чётный путь

Дан ориентированный граф и несколько пар его вершин. Выведите кратчайший путь чётной длины от одной вершины до другой, или -1, если такого пути нет.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
6
A E
A B
B D
B C
C D
D E
2
A E
C B
ВЫВОД
A B C D E
-1

31. Рёбра на кратчайшем пути

Дан неориентированный граф и две его вершины. Выведите все рёбра, которые лежат на каком-либо кратчайшем пути от одной вершины до другой.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
6
A E
A B
B D
B C
C D
D E
B E
ВЫВОД
A B
A E
B D
D E

32. Вершины на кратчайшем пути

Дан неориентированный граф и две его вершины. Выведите все вершины, которые лежат на каком-либо кратчайшем пути от одной вершины до другой (не считая начальной и конечной вершины).

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
6
A E
A B
B D
B C
C D
D E
B E
ВЫВОД
A
D

33. Компоненты сильной связности

Strongly_connected_component.png

Дан ориентированный граф. Подмножество его вершин называется компонентой сильной связности, если между любыми двумя вершинами существует путь в графе, и в то же время ни одна другая вершина не может быть добавлена с сохранением этого свойства.

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

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
14
a b
b e
e a
b f
e f
f g
g f
b c
c g
c d
d c
d h
h d
h g
ВЫВОД
3
a b e
c d h
f g
           
ВВОД:
3
A B
B C
C D
ВЫВОД
4
A
B
C
D
           
ВВОД:
7
A B
B C
C A
B M
M N
N P
P M
ВЫВОД
2
A B C
M N P

34. Поиск мостов

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

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

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
A B
D G
E F
           
ВВОД:
3
A B
B C
C D
ВЫВОД
A B
B C
C D
           
ВВОД:
7
A B
B C
C A
B M
M N
N P
P M
ВЫВОД
B M

35. Поиск точек сочленения

Дан неориентированный граф. Его вершина называется шарниром или точкой сочленения, если после её удаления количество компонент связности увеличивается.

Выведите список точек сочленения, отсортированный в лексикографическом порядке.

Сложность получившегося алгоритма должна быть \(O(|V| + |E|)\).

ВВОД:
8
A B
B C
B D
B E
C E
D E
E F
G D
ВЫВОД
B
D
E
           
ВВОД:
3
A B
B C
C D
ВЫВОД
B
C
           
ВВОД:
7
A B
B C
C A
B M
M N
N P
P M
ВЫВОД
B
M