Подтема 2 из 6
Лучший путь до цели
Почему нельзя перебрать все пути и как найти кратчайший, посчитав для каждой вершины одно число — f(v).
Почему не перебор
Почему бы не выписать все пути, как в учебном графе?
Путей бывает слишком много. В файле задачи темы всего 200 рёбер, а путей из 1 в 100 — почти 150 миллиардов. Каждая развилка умножает число путей: две стрелки на каждом из 30 шагов дают 2³⁰ — больше миллиарда путей. Перебрать их все не успеет ни человек, ни программа.
А если на каждом шаге брать самую дешёвую стрелку? Не сработает. Из 1 самая дешёвая стрелка — в 4, за 2.5. Но из 4 до 100 идти ещё 8.0, всего 10.5. А путь через 7 начинается дороже, с 5.5, и выходит 7.5.
Дешёвый первый шаг ничего не говорит о том, сколько стоит остаток пути. Нужен способ, который учитывает весь остаток, но не перебирает пути по одному.
Одно число на вершину
Что нужно знать о вершине, чтобы решить, идти ли через неё?
Только одно: сколько стоит лучший путь от неё до цели. Как мы в неё пришли, для остатка пути неважно. В вершину 7 можно прийти из 1 или из 6. Но дальше из 7 одна и та же дорога: 7 → 100 за 2.0. Каким бы путём мы ни пришли в 7, лучший остаток стоит 2.
Обозначим это число f(v) — длина кратчайшего пути из вершины v до цели. Здесь f(7) = 2; запись читается «эф от семи». Если знать f для всех вершин, ответ задачи — f(1): длина лучшего пути из 1 в 100.
Правило для f
Как найти f(v), если для соседей f уже известно?
Перебрать стрелки из v: к весу каждой прибавить f вершины, куда она ведёт, и взять наименьшую сумму. В самой цели f = 0 — идти уже некуда. Любой путь из v начинается с какой-то стрелки, а лучший остаток после неё стоит f её конца. Остаётся выбрать лучшую стрелку.
Считаем от цели назад — так у соседей f всегда уже есть:
- f(100) = 0 — мы уже в цели;
- f(7) = 2.0 + f(100) = 2;
- f(4) = 8.0 + f(100) = 8;
- f(1) = min(5.5 + f(7); 12.0 + f(100); 2.5 + f(4)) = min(7.5; 12; 10.5) = 7.5.
f(1) = 7.5 — тот же ответ, что дал перебор, хотя ни один путь целиком мы не выписывали. Посмотри расчёт по шагам: под вершинами появляются значения f, выделена стрелка, которая дала минимум.
- Значения f пока не известны. Начнём с вершин, после которых идти некуда или почти некуда, — с цели.
- Из 12 рёбер нет — до цели не дойти: f(12) = ∞.
- f(100) = 0: мы уже в цели, идти больше не нужно.
- f(7) = 2.0 + f(100) = 2.0 + 0 = 2. Ответы для соседей уже известны — они ближе к цели.
- f(4) = 8.0 + f(100) = 8.0 + 0 = 8. Ответы для соседей уже известны — они ближе к цели.
- f(1) = min(5.5 + f(7), 12.0 + f(100), 2.5 + f(4)) = min(5.5 + 2, 12.0 + 0, 2.5 + 8) = 7.5. Ответы для соседей уже известны — они ближе к цели.
- f(6) = min(7.0 + f(7), 1.0 + f(1)) = min(7.0 + 2, 1.0 + 7.5) = 8.5. Ответы для соседей уже известны — они ближе к цели.
- Готово: f(1) = 7.5. Если из каждой вершины идти по ребру, которое дало минимум, получится путь 1 → 7 → 100.
Правило работает для любой вершины, не только для нужной в ответе: f(6) = min(7.0 + 2; 1.0 + 7.5) = 8.5. А почему у вершины 12 стоит ∞ — в следующем разделе.
Заметь, как устроено правило: чтобы найти f вершины, нужны f её соседей. Функцию, которая для ответа вызывает саму себя, называют . Так мы и запишем f на Python в подтеме 3.
Тупик — бесконечность
Чему равно f(12)? Из 12 не выходит ни одной стрелки.
До цели из 12 не дойти, поэтому f(12) = ∞ — бесконечность, хуже любого настоящего пути. Ноль ставить нельзя: ноль значит «уже пришли».
Посмотрим, к чему приводит ноль. Добавим в учебный граф стрелку 4 → 12 весом 0.5. Если считать f(12) = 0, то f(4) = min(8.0 + 0; 0.5 + 0) = 0.5, а f(1) = 2.5 + 0.5 = 3. Но путь за 3 — это 1 → 4 → 12: он кончается в тупике, а не в 100.
С бесконечностью всё складывается само: ∞ плюс любое число — снова ∞, а любое настоящее число меньше ∞. Поэтому стрелка в тупик никогда не даёт минимум. Бесконечность бывает и у вершины со стрелками — если все они ведут в тупики.
Итого
- Перебор путейпутей бывают миллиарды — не успеть
- Жадный выбордешёвый первый шаг не гарантирует лучший путь
- f(v)длина кратчайшего пути из v до цели
- f(цели)0
- Правилоf(v) = min(вес + f(конца)) по стрелкам из v
- Тупикf = ∞, а не 0
- Учебный примерf(1) = 7.5
Проверьте себя
Ответь сначала сам, потом открой ответ.
ОтветСкрыть ответ
ОтветСкрыть ответ
ОтветСкрыть ответ
ОтветСкрыть ответ
ОтветСкрыть ответ
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Жадный путь из 6
Учебный граф темы — файл из восьми строк:
100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5Иди из вершины 6, каждый раз выбирая самую дешёвую стрелку, пока не придёшь в 100. На сколько такой путь длиннее кратчайшего пути из 6 в 100?
Введите число.
Подсказка
Жадно: 6 → 1 → 4 → 100. Кратчайший путь из 6 найди через 1 и 7.
Решение
3: жадный путь 1.0 + 2.5 + 8.0 = 11.5, кратчайший 6 → 1 → 7 → 100 — 8.5.
Решение программой
from functools import cache EDGES = """100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5"""graph = {}for line in EDGES.splitlines(): 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 greedy = 1.0 + 2.5 + 8.0print(int(greedy - f(6, 100)))Ответ: 3
Через вершину 7
Учебный граф темы — файл из восьми строк:
100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5Найди длину кратчайшего пути из 6 в 100, проходящего через вершину 7. В вершину 7 из 6 можно попасть напрямую или через 1.
Введите длину — десятичную дробь через точку.
Подсказка
Сначала лучший способ попасть в 7, потом f(7) = 2.
Решение
8.5: в 7 дешевле через 1 — 1.0 + 5.5 = 6.5, а не 7.0; дальше 7 → 100 за 2.0.
Решение программой
from functools import cache EDGES = """100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5"""graph = {}for line in EDGES.splitlines(): 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(f(6, 7) + f(7, 100))Ответ: 8.5
f(1) и f(6) учебного графа
Учебный граф темы — файл из восьми строк:
100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5Найди f(1) и f(6) — длины кратчайших путей из 1 и из 6 в вершину 100.
Введите f(1) и f(6) через пробел — десятичные дроби через точку.
Подсказка
От цели назад: f(100) = 0, f(7) = 2, f(4) = 8, потом f(1), потом f(6).
Решение
7.5 8.5: f(1) = min(5.5 + 2; 12 + 0; 2.5 + 8) = 7.5, f(6) = min(7 + 2; 1 + 7.5) = 8.5.
Решение программой
from functools import cache EDGES = """100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.5"""graph = {}for line in EDGES.splitlines(): 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(f(1, 100), f(6, 100))Ответ: 7.5 8.5
f от цели назад с тупиком
Граф задан рёбрами:
1 4 2.51 6 4.14 6 1.24 15 0.66 15 2.96 100 7.415 100 5.815 22 0.3Из вершины 22 рёбер нет. Найди длину кратчайшего пути из 1 в 100. В ответ запиши её целую часть.
Введите целую часть найденной длины.
Подсказка
f(100) = 0, f(22) = ∞: из 22 до цели не дойти. Потом f(15), f(6), f(4) и f(1).
Решение
8: f(15) = 5.8, f(6) = 7.4, f(4) = 6.4, f(1) = min(2.5 + 6.4; 4.1 + 7.4) = 8.9. С ошибкой f(22) = 0 вышло бы 3.4.
Решение программой
from functools import cache EDGES = """1 4 2.51 6 4.14 6 1.24 15 0.66 15 2.96 100 7.415 100 5.815 22 0.3"""graph = {}for line in EDGES.splitlines(): 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)))Ответ: 8
Ноль в тупике
В учебный граф добавили ребро 4 → 12 весом 0.5:
100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.54 12 0.5Из 12 стрелок нет. Какое значение f(1) получится, если ошибочно считать f(12) = 0?
Введите число.
Подсказка
С f(12) = 0 ребро 4 → 12 даёт f(4) = 0.5.
Решение
3: f(4) = min(8.0; 0.5 + 0) = 0.5, f(1) = 2.5 + 0.5 = 3 — длина пути 1 → 4 → 12, который до 100 не доходит. Верно f(12) = ∞ и f(1) = 7.5.
Решение программой
from functools import cache EDGES = """100 12 1.06 7 7.06 1 1.01 7 5.57 100 2.04 100 8.01 100 12.01 4 2.54 12 0.5"""graph = {}for line in EDGES.splitlines(): 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 wrong(v): if v == 100 or v not in graph: return 0 return min(w + wrong(u) for u, w in graph[v]) print(int(wrong(1)))Ответ: 3