Задачи про графы
Здесь предложено несколько задач про обработку графов.
Договорённости
Граф хранится в словаре словарей таким образом, что если такой
словарь связан с переменной graph
, то graph[x][y]
— это
количество рёбер от вершины x
до вершины y
.
Программа должна считывать граф, поданный в следующем формате: в первой строчке — количество рёбер \(N\), в каждой из \(N\) следующих — по паре чисел (начало и конец ребра). Считается, что множество вершин графа — все целые неотрицательные числа.
Вся дополнительная информация подаётся после графа.
Задания
- Применить
print
к внутреннему представлению заданного графа.
Пример:
-- ВХОД --
5
0 1
1 0
0 1
5 6
7 6
-- ВЫХОД --
{0: {1: 2}, 1: {0: 1}, 5: {6: 1}, 7: {6: 1}}
- Найти количество разных путей от вершины \(A\) до вершины \(B\). Эти вершины подаются в строчке после графа. Отсутствие ориентированных циклов гарантируется. Также разрешается применять рекурсию.
Пример:
-- ВХОД --
5
0 1
0 1
0 1
1 2
1 2
0 2
-- ВЫХОД --
6
- Найти какой-нибудь путь от вершины \(A\) до вершины \(B\) методом
поиска в глубину. В графе могут присутствовать ориентированные циклы.
Рекурсию использовать запрещено. Если пути нет, нужно напечатать
НЕТ ПУТИ
.
Пример:
-- ВХОД --
3
1 2
0 1
2 3
1 3
-- ВЫХОД --
1 2 3
- Найти нетривиальные (с более чем одной вершиной) связные компоненты графа. Каждую напечатать в виде возрастающей последовательности номеров её вершин.
Пример:
-- ВХОД --
5
3 2
2 1
1 0
5 6
7 6
-- ВЫХОД --
0 1 2 3
5 6 7
Решения
Задача 1
Определим сначала функцию для считывания графа в том формате, о котором мы договорились выше.
def read_graph_from_stdin():
g = {}
N = int(input())
for i in range(N):
a,b = map(int, input().split())
if a not in g: g[a] = {}
if b in g[a]: g[a][b] += 1
else: g[a][b] = 1
return g
В терминах этой функции решение первой задачи выглядит так:
def problem1():
g = read_graph_from_stdin()
print(g)
Задача 2
Решение второй задачи основывается на следующем: любой путь ненулевой длины
от произвольной вершины v
до пункта назначения представляет собой первый шаг
(в соседнюю вершину u
), за которым следует путь от u
до пункта назначения.
Поэтому количество путей от вершины v
(не совпадающей с пунктом назначения)
до пункта назначения представляет собой сумму количеств путей от соседей v
до пункта назначения.
def problem2():
g = read_graph_from_stdin()
x,y = map(int, input().split())
## словарь, в который мы будем записывать количества путей
path_count = {y: 1} ## от вершины y до неё самой один путь
## функция, считающая количество путей от своего входа до y
def traverse(v):
if v in path_count: return path_count[v]
if v not in g: return 0
s = sum(g[v][n] * traverse(n) for n in g[v])
path_count[v] = s
return s
print(traverse(x))
Отметим, что мы могли обойтись более простой функцией traverse
, не
использующей словарь:
def traverse(v):
if v == y: return 1
if v not in g: return 0
return sum(g[v][n] * traverse(n) for n in g[v])
Основной недостаток такой версии: она каждую вершину посещает столько раз, сколько путей (не учитывающих кратность рёбер) есть до этой вершины. В то время как первая версия каждую вершину посещает столько раз, сколько в неё входит рёбер (без учёта кратностей), если эта вершина вообще достижима.
Задача 3
Для начала определим две вспомогательные функции. Первая функция печатает все элементы входной последовательности друг за другом:
def print_path(p): print(*p)
Напомним, что звёздочка перед формулой, поданной на вход функции, означает, что значение формулы следует воспринимать как последовательность, элементы которой нужно распределить по разным входам функции. Например, формула
foo(1, *[2,3,4])
означает то же самое, что и
foo(1,2,3,4)
Вторая функция выдаёт последовательность соседей указанной вершины графа:
def neighbors(g, v):
if v not in g: return []
else: return g[v].keys()
Эта функция избавляет от необходимости делать лишние проверки вида
if v not in g: ...
которые легко забыть.
Решение третьей задачи представляет собой классический поиск в глубину.
def problem3():
g = read_graph_from_stdin()
x,y = map(int, input().split())
path = [x]
visited = set()
while path:
current_v = path[-1]
visited.add(current_v)
if current_v == y: break
to_visit = [n for n in neighbors(g,current_v)
if n not in visited]
if to_visit: path.append(to_visit[0])
else: path.pop()
if path: print_path(path)
else: print("НЕТ ПУТИ")
Задача 4
Наконец, для решения четвёртой задачи нам пригодятся функции, получающие по графу список его рёбер и множество вершин его рёбер:
def edges(g):
result = []
for a in g:
for b in g[a]:
result.append((a,b))
return result
def vertices(g):
result = set()
for a,b in edges(g):
result.add(a)
result.add(b)
return result
Также определим функции, которая получает на вход отображение компонент, сопоставляющее каждой вершине её компоненту связности (т.е. множество вершин, достижимых из этой вершины), и печатает все различные компоненты связности.
def print_components(components):
visited = set()
for v in components:
if v not in visited:
print(*sorted(components[v]))
visited |= components[v]
Осталось построить такое отображение компонент:
def problem4():
g = read_graph_from_stdin()
## изначально "забываем" про все рёбра;
## в таком случае компонента каждой вершины -- она сама
components = dict((v,{v}) for v in vertices(g))
## теперь по очереди "вспоминаем" рёбра
for a,b in edges(g):
if b not in components[a]:
## объединяем компоненты обоих концов ...
joint_component = (components[a] | components[b])
## ... и обновляем отображение компонент
for x in joint_component:
components[x] = joint_component
print_components(components)