Подтема 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, []))Посмотри, как словарь растёт строка за строкой учебного файла:
- Словарь graph пока пуст. Читаем файл строку за строкой.
- Строка «100 12 1.0»: из 100 в 12 за 1.0. Ключа 100 ещё нет — заводим для него список с этой парой.
- Строка «6 7 7.0»: из 6 в 7 за 7.0. Ключа 6 ещё нет — заводим для него список с этой парой.
- Строка «6 1 1.0»: из 6 в 1 за 1.0. Ключ 6 уже есть — пара встаёт в конец его списка.
- Строка «1 7 5.5»: из 1 в 7 за 5.5. Ключа 1 ещё нет — заводим для него список с этой парой.
- Строка «7 100 2.0»: из 7 в 100 за 2.0. Ключа 7 ещё нет — заводим для него список с этой парой.
- Строка «4 100 8.0»: из 4 в 100 за 8.0. Ключа 4 ещё нет — заводим для него список с этой парой.
- Строка «1 100 12.0»: из 1 в 100 за 12.0. Ключ 1 уже есть — пара встаёт в конец его списка.
- Строка «1 4 2.5»: из 1 в 4 за 2.5. Ключ 1 уже есть — пара встаёт в конец его списка.
- Все строки прочитаны: у каждой вершины — список, куда из неё можно перейти. Из 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))Разберём функцию по строкам:
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). Ответы возвращаются по цепочке обратно. Посмотри по шагам; внизу — стек вызовов: функции, которые начали работу и ждут ответа.
- Вызов f(1): ответа для 1 ещё нет, перебираем рёбра из 1.
- Вызов f(7): ответа для 7 ещё нет, перебираем рёбра из 7.
- f(100): это цель, идти больше не нужно — f(100) = 0. Значение сохранено.
- Ребро 7 → 100 (2): f(100) вернул 0. Кандидат 2 + 0 = 2 — пока лучший.
- Все рёбра из 7 просмотрены: f(7) = 2. Значение сохранено.
- Ребро 1 → 7 (5.5): f(7) вернул 2. Кандидат 5.5 + 2 = 7.5 — пока лучший.
- Ребро 1 → 100 (12): f(100) уже сохранено — берём из памяти: 0. Кандидат 12 + 0 = 12, хуже 7.5.
- Вызов f(4): ответа для 4 ещё нет, перебираем рёбра из 4.
- Ребро 4 → 100 (8): f(100) уже сохранено — берём из памяти: 0. Кандидат 8 + 0 = 8 — пока лучший.
- Все рёбра из 4 просмотрены: f(4) = 8. Значение сохранено.
- Ребро 1 → 4 (2.5): f(4) вернул 8. Кандидат 2.5 + 8 = 10.5, хуже 7.5.
- Все рёбра из 1 просмотрены: f(1) = 7.5. Значение сохранено.
- Ответ: f(1) = 7.5. Каждая вершина вычислена один раз.
Порядок строк в файле функции неважен: она сама идёт по стрелкам. И не зациклится — каждый вложенный вызов это шаг по стрелке, а в графе без циклов бесконечно шагать нельзя.
Зачем @cache
Что будет, если убрать строку @cache?
Ответ тот же, но одни и те же вершины будут считаться снова и снова. запоминает ответ для каждой вершины, и второй раз он берётся из памяти. В учебном графе f(7) нужна дважды: из 6 и из 1. Без запоминания второй раз она пройдёт свой путь заново:
На восьми рёбрах разница мала. Но в файле задачи темы без @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. Посмотри на первую строку вывода: 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
Проверьте себя
Ответь сначала сам, потом открой ответ.
print(graph[100]) и print(graph.get(4, [])) для учебного файла?ОтветСкрыть ответ
[(12, 1.0)] и [(100, 8.0)]: из 100 одна стрелка — в 12, из 4 — в 100.f(12) не падает с ошибкой и что возвращает?ОтветСкрыть ответ
graph.get(12, []) даёт пустой список, цикл не выполняется, и функция возвращает начальное float("inf").if v == 100?ОтветСкрыть ответ
int(f(1)) остановит программу ошибкой.int(12.6) и round(12.6)? Что пойдёт в ответ?ОтветСкрыть ответ
@cache, если вызвать f(5)?ОтветСкрыть ответ
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Строки и числа
Что выведет print("12" + "5", 12 + 5)?
Введите оба значения через пробел.
Подсказка
Строки при сложении склеиваются, числа складываются.
Решение
125 17: «12» + «5» — склейка строк, 12 + 5 — сумма чисел.
Решение программой
print("12" + "5", 12 + 5)Ответ: 125 17
Вершины графа из файла
В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Сколько в графе различных вершин?
Введите число.
Подсказка
Собери оба конца каждого ребра в множество: повторы в нём не сохраняются.
Решение
60 различных вершин.
Решение программой
vertices = set()for line in open("23-1.txt"): a, b, w = line.split() vertices.add(int(a)) vertices.add(int(b))print(len(vertices))Ответ: 60
Кратчайший путь из 1 в 100
В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Найдите длину кратчайшего пути из вершины 1 в вершину 100. В ответ запишите целую часть найденной длины.
Введите целую часть найденной длины.
Подсказка
Словарь из файла и функция f с @cache. Целую часть бери только у итоговой суммы.
Решение
11387: длина кратчайшего пути 11387.63.
Решение программой
from functools import cache graph = {}for line in open("23-1.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, t): if v == t: return 0 best = float("inf") for u, w in graph.get(v, []): best = min(best, w + f(u, t)) return best print(int(f(1, 100)))Ответ: 11387
Кратчайший путь до другой вершины
В текстовом файле 23-1.txt содержится описание ациклического ориентированного взвешенного графа: в каждой строке — номер начальной вершины ребра, номер конечной вершины и вес ребра; числа разделены пробелами и/или табуляциями. Найдите длину кратчайшего пути из вершины 1 в вершину 255. В ответ запишите целую часть найденной длины.
Введите целую часть найденной длины.
Подсказка
Меняется только цель: сравнивай v с 255.
Решение
11009.
Решение программой
from functools import cache graph = {}for line in open("23-1.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, t): if v == t: return 0 best = float("inf") for u, w in graph.get(v, []): best = min(best, w + f(u, t)) return best print(int(f(1, 255)))Ответ: 11009
Дроби во float
Что выведет программа?
print(0.1 + 0.2 == 0.3, int(2.8 + 3.6))Введите оба значения через пробел, как их выведет Python.
Подсказка
0.1 и 0.2 хранятся приближённо. int отбрасывает дробную часть суммы.
Решение
False 6: сумма float — 0.30000000000000004, а 2.8 + 3.6 = 6.4, целая часть 6.
Решение программой
print(0.1 + 0.2 == 0.3, int(2.8 + 3.6))Ответ: False 6
Задача темы: кратчайший путь по файлу 23-5
Задача темы. В текстовом файле 23-5.txt содержится описание ациклического ориентированного взвешенного графа. В каждой строке файла записаны два натуральных числа (L, M) и одно положительное вещественное число (W). L и M — номера вершин графа, W — вес ребра, ведущего из вершины L в вершину M. Таким образом, количество строк в файле равно количеству рёбер в графе. Две вершины графа не могут быть соединены более чем одним ребром. Найдите и запишите в ответе целую часть длины кратчайшего пути из вершины с номером 1 в вершину с номером 100. Существование хотя бы одного такого пути гарантируется. Под длиной кратчайшего пути понимается минимальная сумма весов рёбер, составляющих путь. Вершины графа могут быть пронумерованы не подряд. L ≤ 1000, M ≤ 1000; W ≤ 10 000. Количество строк в файле не превосходит 200. Числа в строках разделены произвольным ненулевым количеством пробелов и/или табуляций.
Введите целую часть найденной длины.
Подсказка
Граф в словарь, f(v) с @cache, в ответ — int(f(1)).
Решение
10211: длина кратчайшего пути 10211.63 (Python выводит 10211.630000000003).
Решение программой
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, t): if v == t: return 0 best = float("inf") for u, w in graph.get(v, []): best = min(best, w + f(u, t)) return best print(int(f(1, 100)))Ответ: 10211