К содержанию

Подтема 3 из 6

Программа с @cache

Граф из файла — в словарь, правило для f — в рекурсивную функцию с @cache; решаем задачу темы.

Граф из файла

Как превратить строки файла в граф, по которому можно ходить?

Разложить рёбра по начальным вершинам в : ключ — вершина, значение — список пар (куда, вес). Тогда все стрелки из вершины v — это graph[v].

Каждую строку разбиваем : без аргумента он делит по любым пробелам и табуляциям, сколько бы их ни было. Получаются три куска текста. Номера вершин превращаем в int, вес — в float: он дробный. Заголовка в файле нет — первая строка тоже ребро.

graph = {}for line in open("23.txt"):    a, b, w = line.split()    a, b, w = int(a), int(b), float(w)    if a not in graph:        graph[a] = []    graph[a].append((b, w)) print(graph[1])print(graph.get(12, []))
[(7, 5.5), (100, 12.0), (4, 2.5)] []

Посмотри, как словарь растёт строка за строкой учебного файла:

  1. Словарь graph пока пуст. Читаем файл строку за строкой.
  2. Строка «100 12 1.0»: из 100 в 12 за 1.0. Ключа 100 ещё нет — заводим для него список с этой парой.
  3. Строка «6 7 7.0»: из 6 в 7 за 7.0. Ключа 6 ещё нет — заводим для него список с этой парой.
  4. Строка «6 1 1.0»: из 6 в 1 за 1.0. Ключ 6 уже есть — пара встаёт в конец его списка.
  5. Строка «1 7 5.5»: из 1 в 7 за 5.5. Ключа 1 ещё нет — заводим для него список с этой парой.
  6. Строка «7 100 2.0»: из 7 в 100 за 2.0. Ключа 7 ещё нет — заводим для него список с этой парой.
  7. Строка «4 100 8.0»: из 4 в 100 за 8.0. Ключа 4 ещё нет — заводим для него список с этой парой.
  8. Строка «1 100 12.0»: из 1 в 100 за 12.0. Ключ 1 уже есть — пара встаёт в конец его списка.
  9. Строка «1 4 2.5»: из 1 в 4 за 2.5. Ключ 1 уже есть — пара встаёт в конец его списка.
  10. Все строки прочитаны: у каждой вершины — список, куда из неё можно перейти. Из 12 рёбер нет: ключа нет, и graph.get вернёт пустой список.

Из 12 стрелок нет, поэтому и ключа 12 в словаре нет. graph[12] остановил бы программу ошибкой KeyError, а возвращает пустой список — для тупика это и нужно.

Функция f

Как записать правило для f на Python?

Почти дословно: в цели вернуть 0, иначе перебрать стрелки из v и взять наименьшее «вес + f(конца)». Функция вызывает сама себя для соседей — это . Словарь graph уже прочитан из файла, как в разделе выше; cache подключаем первой строкой программы.

from functools import cache  @cachedef f(v):    if v == 100:        return 0    best = float("inf")    for u, w in graph.get(v, []):        best = min(best, w + f(u))    return best  print(f(1), f(6), f(12))
7.5 8.5 inf

Разберём функцию по строкам:

  • if v == 100: return 0 — мы в цели, идти больше некуда.
  • best = float("inf") — пока ни одного пути до цели не найдено: бесконечность.
  • for u, w in graph.get(v, []) — каждая стрелка v → u весом w. У тупика список пуст, цикл не выполнится, и функция вернёт бесконечность — как в подтеме 2.
  • best = min(best, w + f(u)) — шаг по стрелке плюс лучший остаток пути от u.

Руками мы считали от цели назад, а функция начинает с другого конца: f(1) просит f(7), та — f(100). Ответы возвращаются по цепочке обратно. Посмотри по шагам; внизу — стек вызовов: функции, которые начали работу и ждут ответа.

  1. Вызов f(1): ответа для 1 ещё нет, перебираем рёбра из 1.
  2. Вызов f(7): ответа для 7 ещё нет, перебираем рёбра из 7.
  3. f(100): это цель, идти больше не нужно — f(100) = 0. Значение сохранено.
  4. Ребро 7 → 100 (2): f(100) вернул 0. Кандидат 2 + 0 = 2 — пока лучший.
  5. Все рёбра из 7 просмотрены: f(7) = 2. Значение сохранено.
  6. Ребро 1 → 7 (5.5): f(7) вернул 2. Кандидат 5.5 + 2 = 7.5 — пока лучший.
  7. Ребро 1 → 100 (12): f(100) уже сохранено — берём из памяти: 0. Кандидат 12 + 0 = 12, хуже 7.5.
  8. Вызов f(4): ответа для 4 ещё нет, перебираем рёбра из 4.
  9. Ребро 4 → 100 (8): f(100) уже сохранено — берём из памяти: 0. Кандидат 8 + 0 = 8 — пока лучший.
  10. Все рёбра из 4 просмотрены: f(4) = 8. Значение сохранено.
  11. Ребро 1 → 4 (2.5): f(4) вернул 8. Кандидат 2.5 + 8 = 10.5, хуже 7.5.
  12. Все рёбра из 1 просмотрены: f(1) = 7.5. Значение сохранено.
  13. Ответ: f(1) = 7.5. Каждая вершина вычислена один раз.

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

Зачем @cache

Что будет, если убрать строку @cache?

Ответ тот же, но одни и те же вершины будут считаться снова и снова. запоминает ответ для каждой вершины, и второй раз он берётся из памяти. В учебном графе f(7) нужна дважды: из 6 и из 1. Без запоминания второй раз она пройдёт свой путь заново:

Вызовы f(6). Без @cache f(7) считается дважды, f(100) — четыре раза. С @cache повторный вызов сразу берёт ответ из памяти.

На восьми рёбрах разница мала. Но в файле задачи темы без @cache функция была бы вызвана почти триллион раз: она прошла бы по каждому из 150 миллиардов путей. Программа не закончит и за сутки. С @cache каждая из 59 вершин, до которых можно дойти из 1, считается один раз.

cache берут из модуля functools: первая строка программы — from functools import cache, а @cache ставят прямо над def.

Тонкости:большой файл и глубина рекурсии

В файле экзамена не больше 200 строк, и рекурсия уходит не глубже 200 вызовов. Но в тренировочных задачах встречаются файлы на 100 000 рёбер с длинными цепочками. Тогда Python останавливает программу ошибкой RecursionError: по умолчанию он разрешает около тысячи вложенных вызовов. Помогает строка в начале программы: после import sys.

Задача темы

Решим задачу темы целиком:

«В текстовом файле 23-5.txt содержится описание ациклического ориентированного взвешенного графа. В каждой строке файла записаны два натуральных числа (L, M) и одно положительное вещественное число (W). L и M — номера вершин графа, W — вес ребра, ведущего из вершины L в вершину M. Таким образом, количество строк в файле равно количеству рёбер в графе. Две вершины графа не могут быть соединены более чем одним ребром. Найдите и запишите в ответе целую часть длины кратчайшего пути из вершины с номером 1 в вершину с номером 100. Существование хотя бы одного такого пути гарантируется. Под длиной кратчайшего пути понимается минимальная сумма весов рёбер, составляющих путь. Вершины графа могут быть пронумерованы не подряд. L ≤ 1000, M ≤ 1000; W ≤ 10 000. Количество строк в файле не превосходит 200. Числа в строках разделены произвольным ненулевым количеством пробелов и/или табуляций.»

Программа та же, меняется только имя файла и последняя строка: ответ — целая часть f(1). Файл 23-5.txt приложен к задаче темы в практике этой подтемы — сохрани его рядом с программой.

from functools import cache graph = {}for line in open("23-5.txt"):    a, b, w = line.split()    a, b, w = int(a), int(b), float(w)    if a not in graph:        graph[a] = []    graph[a].append((b, w))  @cachedef f(v):    if v == 100:        return 0    best = float("inf")    for u, w in graph.get(v, []):        best = min(best, w + f(u))    return best  print(f(1))print(int(f(1)))
10211.630000000003 10211

Ответ — 10211. Посмотри на первую строку вывода: 10211.630000000003 вместо 10211.63. Дроби в хранятся приближённо, и при сложении копятся крошечные ошибки. На целую часть они не влияют, пока сумма не стоит вплотную к целому числу.

Тонкости:целая часть, round и почти целые суммы

int отбрасывает дробную часть: int(10211.63) — 10211. округляет и дал бы 10212 — неверный ответ.

Отбрасывать дробь можно только у итоговой суммы. Если отбросить её у каждого веса при чтении, ответ уплывёт: 2.8 + 3.6 = 6.4, а 2 + 3 = 5.

Если вывод вышел вроде 4519.999999999998, настоящая сумма — 4520: это ошибка float, и ответ 4520. Поэтому сначала выведи само число, а потом его целую часть.

Итого

  • Графсловарь: вершина → список пар (куда, вес)
  • Строка файлаline.split(); номера — int, вес — float
  • Тупикgraph.get(v, []) — пустой список, f = ∞
  • f(v)в цели 0, иначе min(w + f(u)) по стрелкам из v
  • @cacheкаждая вершина считается один раз
  • Ответint(f(1)) — в самом конце, не round
  • Задача темы10211

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1
Что выведут print(graph[100]) и print(graph.get(4, [])) для учебного файла?
ОтветСкрыть ответ
Ответ
[(12, 1.0)] и [(100, 8.0)]: из 100 одна стрелка — в 12, из 4 — в 100.
Вопрос 2
Ключа 12 в словаре нет. Почему f(12) не падает с ошибкой и что возвращает?
ОтветСкрыть ответ
Ответ
graph.get(12, []) даёт пустой список, цикл не выполняется, и функция возвращает начальное float("inf").
Вопрос 3
Что случится, если убрать из функции проверку if v == 100?
ОтветСкрыть ответ
Ответ
Цель станет обычной вершиной: из 100 стрелка ведёт в тупик 12, и f(100) = ∞. Тогда бесконечными станут все значения, а int(f(1)) остановит программу ошибкой.
Вопрос 4
Длина кратчайшего пути — 12.6. Что выведут int(12.6) и round(12.6)? Что пойдёт в ответ?
ОтветСкрыть ответ
Ответ
12 и 13. В ответ — 12: нужна целая часть.
Вопрос 5
В графе есть стрелки 5 → 8 и 8 → 5. Поможет ли @cache, если вызвать f(5)?
ОтветСкрыть ответ
Ответ
Нет. f(5) вызовет f(8), а та — снова f(5), ответ для которой ещё не готов: запоминать нечего. Вызовы не кончатся, и Python остановит программу ошибкой RecursionError.

Практика

Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.

Строки и числа

Что выведет print("12" + "5", 12 + 5)?

Введите оба значения через пробел.

Вершины графа из файла

В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Сколько в графе различных вершин?

23-1.txtАциклический ориентированный граф: в строке — начало ребра, конец и весТекстовый файл · 3,2 КБ

Введите число.

Кратчайший путь из 1 в 100

В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Найдите длину кратчайшего пути из вершины 1 в вершину 100. В ответ запишите целую часть найденной длины.

23-1.txtАциклический ориентированный граф: в строке — начало ребра, конец и весТекстовый файл · 3,2 КБ

Введите целую часть найденной длины.

Кратчайший путь до другой вершины

В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Найдите длину кратчайшего пути из вершины 1 в вершину 255. В ответ запишите целую часть найденной длины.

23-1.txtАциклический ориентированный граф: в строке — начало ребра, конец и весТекстовый файл · 3,2 КБ

Введите целую часть найденной длины.

Дроби во float

Что выведет программа?

print(0.1 + 0.2 == 0.3, int(2.8 + 3.6))

Введите оба значения через пробел, как их выведет Python.

Задача темы: кратчайший путь по файлу 23-5

Задача темы. В текстовом файле 23-5.txt содержится описание ациклического ориентированного взвешенного графа. В каждой строке файла записаны два натуральных числа (L, M) и одно положительное вещественное число (W). L и M — номера вершин графа, W — вес ребра, ведущего из вершины L в вершину M. Таким образом, количество строк в файле равно количеству рёбер в графе. Две вершины графа не могут быть соединены более чем одним ребром. Найдите и запишите в ответе целую часть длины кратчайшего пути из вершины с номером 1 в вершину с номером 100. Существование хотя бы одного такого пути гарантируется. Под длиной кратчайшего пути понимается минимальная сумма весов рёбер, составляющих путь. Вершины графа могут быть пронумерованы не подряд. L ≤ 1000, M ≤ 1000; W ≤ 10 000. Количество строк в файле не превосходит 200. Числа в строках разделены произвольным ненулевым количеством пробелов и/или табуляций.

23-5.txtАциклический ориентированный граф: в строке — начало ребра, конец и весТекстовый файл · 3,2 КБ

Введите целую часть найденной длины.