425 lines
20 KiB
Python
425 lines
20 KiB
Python
"""
|
||
https://stepik.org/lesson/724551/step/1?unit=725686
|
||
|
||
Испытание "Бремя наследия"
|
||
|
||
Необходимо написать универсальную основу для представления ненаправленных связных графов и поиска в них кратчайших маршрутов.
|
||
Далее, этот алгоритм предполагается применять для прокладки маршрутов: на картах, в метро и так далее.
|
||
|
||
Для универсального описания графов, вам требуется объявить в программе следующие классы:
|
||
Vertex - для представления вершин графа (на карте это могут быть: здания, остановки, достопримечательности и т.п.);
|
||
Link - для описания связи между двумя произвольными вершинами графа (на карте: маршруты, время в пути и т.п.);
|
||
LinkedGraph - для представления связного графа в целом (карта целиком).
|
||
|
||
Объекты класса Vertex должны создаваться командой:
|
||
>>> v = Vertex()
|
||
|
||
и содержать локальный атрибут:
|
||
_links - список связей с другими вершинами графа (список объектов класса Link).
|
||
|
||
Также в этом классе должно быть объект-свойство (property):
|
||
links - для получения ссылки на список _links.
|
||
|
||
Объекты следующего класса Link должны создаваться командой:
|
||
>>> v1 = Vertex(); v2 = Vertex()
|
||
>>> link = Link(v1, v2)
|
||
|
||
где v1, v2 - объекты класса Vertex (вершины графа). Внутри каждого объекта класса Link должны формироваться следующие локальные атрибуты:
|
||
|
||
_v1, _v2 - ссылки на объекты класса Vertex, которые соединяются данной связью;
|
||
_dist - длина связи (по умолчанию 1); это может быть длина пути, время в пути и др.
|
||
|
||
В классе Link должны быть объявлены следующие объекты-свойства:
|
||
v1 - для получения ссылки на вершину v1;
|
||
v2 - для получения ссылки на вершину v2;
|
||
dist - для изменения и считывания значения атрибута _dist.
|
||
|
||
Наконец, объекты третьего класса LinkedGraph должны создаваться командой:
|
||
>>> map_graph = LinkedGraph()
|
||
|
||
В каждом объекте класса LinkedGraph должны формироваться локальные атрибуты:
|
||
_links - список из всех связей графа (из объектов класса Link);
|
||
_vertex - список из всех вершин графа (из объектов класса Vertex).
|
||
|
||
В самом классе LinkedGraph необходимо объявить (как минимум) следующие методы:
|
||
def add_vertex(self, v): ... - для добавления новой вершины v в список _vertex (если она там отсутствует);
|
||
def add_link(self, link): ... - для добавления новой связи link в список _links (если объект link с указанными вершинами в списке отсутствует);
|
||
def find_path(self, start_v, stop_v): ... - для поиска кратчайшего маршрута из вершины start_v в вершину stop_v.
|
||
|
||
Метод find_path() должен возвращать список из вершин кратчайшего маршрута и список из связей этого же маршрута в виде кортежа:
|
||
([вершины кратчайшего пути], [связи между вершинами])
|
||
|
||
Поиск кратчайшего маршрута допустимо делать полным перебором с помощью рекурсивной функции (будем полагать, что общее число вершин в графе не превышает 100).
|
||
Для тех, кто желает испытать себя в полной мере, можно реализовать алгоритм Дейкстры поиска кратчайшего пути в связном взвешенном графе.
|
||
В методе add_link() при добавлении новой связи следует автоматически добавлять вершины этой связи в список _vertex, если они там отсутствуют.
|
||
|
||
Проверку наличия связи в списке _links следует определять по вершинам этой связи. Например, если в списке имеется объект:
|
||
_links = [Link(v1, v2)]
|
||
|
||
то добавлять в него новые объекты Link(v2, v1) или Link(v1, v2) нельзя (обратите внимание у всех трех объектов будут разные id, т.е. по id определять вхождение в список нельзя).
|
||
Подсказка: проверку на наличие существующей связи можно выполнить с использованием функции filter() и указанием нужного условия для отбора объектов.
|
||
|
||
Пример использования классов, применительно к схеме метро (эти строчки в программе писать не нужно):
|
||
|
||
>>> Vertex._num = 0
|
||
>>> map_graph = LinkedGraph()
|
||
>>> v1 = Vertex()
|
||
>>> v2 = Vertex()
|
||
>>> v3 = Vertex()
|
||
>>> v4 = Vertex()
|
||
>>> v5 = Vertex()
|
||
>>> v6 = Vertex()
|
||
>>> v7 = Vertex()
|
||
>>> map_graph.add_link(Link(v1, v2))
|
||
>>> map_graph.add_link(Link(v2, v3))
|
||
>>> map_graph.add_link(Link(v1, v3))
|
||
>>> map_graph.add_link(Link(v4, v5))
|
||
>>> map_graph.add_link(Link(v6, v7))
|
||
>>> map_graph.add_link(Link(v2, v7))
|
||
>>> map_graph.add_link(Link(v3, v4))
|
||
>>> map_graph.add_link(Link(v5, v6))
|
||
>>> len(map_graph._links)
|
||
8
|
||
>>> len(map_graph._vertex)
|
||
7
|
||
>>> map_graph.find_path(v1, v6)
|
||
([Vertex('A'), Vertex('B'), Vertex('G'), Vertex('F')], [Link(Vertex('A'), Vertex('B'), 1), Link(Vertex('B'), Vertex('G'), 1), Link(Vertex('F'), Vertex('G'), 1)])
|
||
|
||
Однако, в таком виде применять классы для схемы карты метро не очень удобно.
|
||
Например, здесь нет указаний названий станций, а также длина каждого сегмента равна 1, что не соответствует действительности.
|
||
Чтобы поправить этот момент и реализовать программу поиска кратчайшего пути в метро между двумя произвольными станциями, объявите еще два дочерних класса:
|
||
|
||
class Station(Vertex): ... - для описания станций метро;
|
||
class LinkMetro(Link): ... - для описания связей между станциями метро.
|
||
|
||
Объекты класса Station должны создаваться командой:
|
||
>>> st = Station(name := "Домодедовская")
|
||
|
||
где name - название станции (строка). В каждом объекте класса Station должен дополнительно формироваться локальный атрибут:
|
||
name - название станции метро.
|
||
|
||
(Не забудьте в инициализаторе дочернего класса вызывать инициализатор базового класса).
|
||
В самом классе Station переопределите магические методы __str__() и __repr__(), чтобы они возвращали название станции метро (локальный атрибут name).
|
||
|
||
Объекты второго класса LinkMetro должны создаваться командой:
|
||
>>> link = LinkMetro(v1, v2, dist := 2)
|
||
|
||
где v1, v2 - вершины (станции метро); dist - расстояние между станциями (любое положительное число).
|
||
(Также не забывайте в инициализаторе этого дочернего класса вызывать инициализатор базового класса).
|
||
|
||
В результате, эти классы должны совместно работать следующим образом (эти строчки в программе писать не нужно):
|
||
|
||
>>> map_metro = LinkedGraph()
|
||
>>> v1 = Station("Сретенский бульвар")
|
||
>>> v2 = Station("Тургеневская")
|
||
>>> v3 = Station("Чистые пруды")
|
||
>>> v4 = Station("Лубянка")
|
||
>>> v5 = Station("Кузнецкий мост")
|
||
>>> v6 = Station("Китай-город 1")
|
||
>>> v7 = Station("Китай-город 2")
|
||
|
||
>>> map_metro.add_link(LinkMetro(v1, v2, 1))
|
||
>>> map_metro.add_link(LinkMetro(v2, v3, 1))
|
||
>>> map_metro.add_link(LinkMetro(v1, v3, 1))
|
||
|
||
>>> map_metro.add_link(LinkMetro(v4, v5, 1))
|
||
>>> map_metro.add_link(LinkMetro(v6, v7, 1))
|
||
|
||
>>> map_metro.add_link(LinkMetro(v2, v7, 5))
|
||
>>> map_metro.add_link(LinkMetro(v3, v4, 3))
|
||
>>> map_metro.add_link(LinkMetro(v5, v6, 3))
|
||
|
||
>>> print(len(map_metro._links))
|
||
8
|
||
>>> print(len(map_metro._vertex))
|
||
7
|
||
>>> path = map_metro.find_path(v1, v6) # от сретенского бульвара до китай-город 1
|
||
>>> print(path[0]) # [Сретенский бульвар, Тургеневская, Китай-город 2, Китай-город 1]
|
||
[Сретенский бульвар, Тургеневская, Китай-город 2, Китай-город 1]
|
||
>>> print(sum([x.dist for x in path[1]])) # 7
|
||
7
|
||
|
||
P.S. В программе нужно объявить только классы Vertex, Link, LinkedGraph, Station, LinkMetro. На экран ничего выводить не нужно.
|
||
"""
|
||
|
||
from collections import defaultdict
|
||
from dataclasses import dataclass, field
|
||
from functools import total_ordering
|
||
from typing import List, Optional, Tuple, Union
|
||
|
||
|
||
def make_properties_prot(*names):
|
||
def decorator(cls):
|
||
def prop(private_name: str):
|
||
def getter(self):
|
||
return getattr(self, private_name)
|
||
|
||
def setter(self, value):
|
||
return setattr(self, private_name, value)
|
||
|
||
return getter, setter
|
||
|
||
for name in names:
|
||
setattr(cls, name, property(*prop(f"_{name}")))
|
||
return cls
|
||
|
||
return decorator
|
||
|
||
|
||
class AutoNamed:
|
||
_num: int = 0
|
||
name: str
|
||
|
||
@staticmethod
|
||
def column_code(num: int) -> str:
|
||
"""Имя как код колонки в Excel по порядковому номеру с 1"""
|
||
|
||
def gen(n: int):
|
||
a = ord("A")
|
||
sz = ord("Z") - a + 1
|
||
while n:
|
||
n, mod = divmod(n - 1, sz)
|
||
yield chr(mod + a)
|
||
|
||
return "".join(gen(abs(int(num))))[::-1]
|
||
|
||
def __new__(cls, *args, **kwargs):
|
||
obj = super().__new__(cls)
|
||
cls._num += 1
|
||
obj.name = cls.column_code(cls._num)
|
||
return obj
|
||
|
||
|
||
@make_properties_prot("links")
|
||
class Vertex(AutoNamed):
|
||
def __init__(self, name: Optional[str] = None):
|
||
if name:
|
||
self.name = name
|
||
self._links = []
|
||
|
||
def __repr__(self) -> str:
|
||
return f"{self.__class__.__name__}({self.name!r})"
|
||
|
||
def add_link(self, link):
|
||
if link not in self._links:
|
||
self._links.append(link)
|
||
|
||
def __len__(self):
|
||
return len(self.links)
|
||
|
||
def __getitem__(self, key):
|
||
return self.links[key]
|
||
|
||
|
||
@make_properties_prot("v1", "v2", "dist")
|
||
class Link:
|
||
def __init__(self, v1: Vertex, v2: Vertex, dist: Union[int, float] = 1):
|
||
self._v1, self._v2, self._dist = v1, v2, dist
|
||
for x in self:
|
||
x.add_link(self)
|
||
|
||
def __len__(self):
|
||
return 2
|
||
|
||
def __getitem__(self, key):
|
||
return (self.v1, self.v2)[key]
|
||
|
||
def __repr__(self) -> str:
|
||
return f"{self.__class__.__name__}{(self.v1, self.v2, self.dist)!r}"
|
||
|
||
def __hash__(self):
|
||
return hash(frozenset((self.v1, self.v2, self.dist)))
|
||
|
||
def __eq__(self, other):
|
||
return hash(self) == hash(other)
|
||
|
||
|
||
@dataclass
|
||
@total_ordering
|
||
class LinksPath:
|
||
links: List[Link] = field(default_factory=list)
|
||
is_start: bool = field(init=False, default=False)
|
||
|
||
@property
|
||
def dist(self):
|
||
if self.is_start:
|
||
return 0
|
||
if self.links:
|
||
return sum([x.dist for x in self.links])
|
||
return float("inf")
|
||
|
||
def add_link(self, link: Link):
|
||
if link not in self.links:
|
||
self.links.append(link)
|
||
return self
|
||
|
||
def copy(self):
|
||
return self.__class__(self.links[:])
|
||
|
||
@property
|
||
def vertex(self):
|
||
return [*{v: v for lnk in self.links for v in lnk}.keys()]
|
||
|
||
def __eq__(self, other) -> bool:
|
||
if isinstance(other, self.__class__):
|
||
return self.dist == other.dist
|
||
return self.dist == other
|
||
|
||
def __le__(self, other) -> bool:
|
||
if isinstance(other, self.__class__):
|
||
return self.dist < other.dist
|
||
return self.dist < other
|
||
|
||
|
||
@make_properties_prot("links", "vertex")
|
||
class LinkedGraph:
|
||
def __init__(self, vertex: Optional[Vertex] = None, links: Optional[Link] = None):
|
||
self._vertex = vertex or []
|
||
self._links = links or []
|
||
|
||
def __repr__(self) -> str:
|
||
return f"{self.__class__.__name__}{(self.vertex, self.links)!r}"
|
||
|
||
def add_vertex(self, v: Vertex):
|
||
if v not in self._vertex:
|
||
self._vertex.append(v)
|
||
|
||
def add_link(self, link: Link):
|
||
if link not in self._links:
|
||
self._links.append(link)
|
||
for v in link:
|
||
self.add_vertex(v)
|
||
|
||
def dijkstras(self, start: Vertex, stop: Optional[Vertex] = None):
|
||
def walk(remaining, paths, current):
|
||
while remaining:
|
||
remaining.discard(current)
|
||
yield current
|
||
if current == stop:
|
||
break
|
||
if remaining:
|
||
current = min(remaining, key=lambda x: paths[x])
|
||
|
||
paths = defaultdict(LinksPath)
|
||
paths[start].is_start = True
|
||
remaining = set(self.vertex)
|
||
for current in walk(remaining, paths, start):
|
||
for link in current:
|
||
for v in filter(lambda v: v in remaining, link):
|
||
new_path = paths[current].copy().add_link(link)
|
||
paths[v] = min(paths[v], new_path)
|
||
return paths
|
||
|
||
def find_path(
|
||
self, start_v: Vertex, stop_v: Vertex
|
||
) -> Tuple[List[Vertex], List[Link]]:
|
||
path = self.dijkstras(start_v, stop_v)[stop_v]
|
||
return path.vertex, path.links
|
||
|
||
|
||
class Station(Vertex):
|
||
def __repr__(self):
|
||
return self.name
|
||
|
||
|
||
class LinkMetro(Link):
|
||
...
|
||
|
||
|
||
print("-------------------------------------------")
|
||
|
||
vA, vB, vC, vD, vE, vF, vG = [Vertex() for _ in range(7)]
|
||
map_graph = LinkedGraph()
|
||
map_graph.add_link(Link(vA, vB))
|
||
map_graph.add_link(Link(vB, vC))
|
||
map_graph.add_link(Link(vA, vC))
|
||
|
||
map_graph.add_link(Link(vD, vE))
|
||
map_graph.add_link(Link(vF, vG))
|
||
|
||
map_graph.add_link(Link(vB, vG))
|
||
map_graph.add_link(Link(vC, vD))
|
||
map_graph.add_link(Link(vE, vF))
|
||
|
||
print(len(map_graph._links)) # 8 связей
|
||
print(len(map_graph._vertex)) # 7 вершин
|
||
print(map_graph.find_path(vA, vG))
|
||
# ([Vertex('A'), Vertex('B'), Vertex('G')], [Link(Vertex('A'), Vertex('B'), 1), Link(Vertex('B'), Vertex('G'), 1)])
|
||
|
||
print("-------------------------------------------")
|
||
|
||
map_metro = LinkedGraph()
|
||
v1 = Station("Сретенский бульвар")
|
||
v2 = Station("Тургеневская")
|
||
v3 = Station("Чистые пруды")
|
||
v4 = Station("Лубянка")
|
||
v5 = Station("Кузнецкий мост")
|
||
v6 = Station("Китай-город 1")
|
||
v7 = Station("Китай-город 2")
|
||
|
||
map_metro.add_link(LinkMetro(v1, v2, 1))
|
||
map_metro.add_link(LinkMetro(v2, v3, 1))
|
||
map_metro.add_link(LinkMetro(v1, v3, 1))
|
||
|
||
map_metro.add_link(LinkMetro(v4, v5, 1))
|
||
map_metro.add_link(LinkMetro(v6, v7, 1))
|
||
|
||
map_metro.add_link(LinkMetro(v2, v7, 5))
|
||
map_metro.add_link(LinkMetro(v3, v4, 3))
|
||
map_metro.add_link(LinkMetro(v5, v6, 3))
|
||
|
||
print(len(map_metro._links))
|
||
print(len(map_metro._vertex))
|
||
path = map_metro.find_path(v1, v6) # от сретенского бульвара до китай-город 1
|
||
print(path[0]) # [Сретенский бульвар, Тургеневская, Китай-город 2, Китай-город 1]
|
||
print(sum([x.dist for x in path[1]])) # 7
|
||
|
||
print("-------------------------------------------", flush=True)
|
||
Vertex._num = 0 # naming reset
|
||
|
||
# exit(1)
|
||
|
||
|
||
def tests():
|
||
code = (
|
||
b"ZDDXSAUz;VX>My}WJhvgaA+tg3U)CdJs?(Pa&%>QC@BhdG9W!5R%LQ@Wq2ql3U)IfJs?(Pa&%>"
|
||
+ b"QC@BhdG$1`7R%LQ@Wq2ql3U)OhJs?(Pa&%>QC@BgGZDDXSE@5P3Uu<b^YbZ=<ZfhuZF)Sc<GAStv"
|
||
+ b"ZDDXSE@5P3Uu<b^YbZ=<ZfhuZGAtl=Gbt$wZDDXSE@5P3Uu<b^YbZ=<ZfhuZGAtl=G$|<xZDDXSE"
|
||
+ b"@5P3Uu<b^YbZ=<ZfhuZGb|u>G$|<xZDDXSE@5P3Uu<b^YbZ=<ZfhuZG%O%?H7O|y3So0|WpZ>NY-"
|
||
+ b"MgJZDDXSE?;bEZfkQXAU!=GH7p<^(7n*L(6Z3A(SXps(7w>MAkl}=xY2>oyU@NM(Sgvi(T~u#(6!"
|
||
+ b"LHAkeZP(Sgvv(74fo(7MpIAYW{0ZfkQO(7MpO(6G^g(SgvgAWUg)Yh`3da$#_2A_`%1b7gXLAZ%r"
|
||
+ b"BC~aYIGA>_sWpZ?7cqt$~Js>qKAR^Gc(6!LA(6!Nk(7n*U(6u1ZhtRmufzZ3qz97)D(6!Nk(TLEv"
|
||
+ b"(7hngvLMlc(7({Q(Sgvq(6u06c4cyOWq2Uay3o7Ou+f3hfzYrZOlfXwWn@QkVQ^?73JPsua564oW"
|
||
+ b"Mp4#X>MyMOlfXwD0VU|Aa*e+DGFh8b7gXLAZ%rBC~aYIGA>_iX>MzCDIh&PAT=x?BGA0hwb6pmzR"
|
||
+ b"<KFVPs@qY-w(5C@CP&w9vlLvCy#4ve3BDyCBhl(6Z5w(6`ZyAWUg)YbbUyEFg9<DJ&q-h0wmyg3*"
|
||
+ b"s4(Sy*o(6u1YxX`@Nwa~TEg3*D|k08;3(6Z5w(6`ZyAWUg)YbbUxEFg9=DIy9AaA9<4AUz;$VQ?}"
|
||
+ b"oW@&C@UvOb`Xef3uEFg9@DGGBSJs@*+Z75rKE@WwQbRcGLav*phX>K5JVRUF)F<o6L3So0|WpZ>N"
|
||
+ b"b09rEATul=BGA3iwa~KAwb6jkz0k1Hk08;3(Sy*u(7e#F(SXps(6G^uAkehXyU@7Mz0j~A(7e#F("
|
||
+ b"SXs2(SXr|(Sp#hEFjRb(7w>O(7e#T(6-RM(7r4n(7n*L(7MpR(SXr_(6!LI(Sp#u(7qtifY7kevC"
|
||
+ b"zKJg3z$gwb6ng(7w>I(TdQu(7MrrEzyC{ve3TJxzT~qg3z+iz93|2b95pK3So0|WpZ>NX>)URVq<"
|
||
+ b"J!b8{$DbYXO9Z*D9gR%LQ@Wq2tdVQyp~X>)URVq<J!b8{$6X>MyxWpr|HEFes2ZfhwlAR^GZ(7Vv"
|
||
+ b"E(Sgx{AX9W<bZKvHAkehXzR<hSw$QcEy&%xN(6G^g(7VvJ(6rFL(6Z35(Sp&8(SgyAAke<if*{bk"
|
||
+ b"(7VvE(Sgx{(6As@WpZ?7cq|~$uprR7(7VvE(Sgx{AWUg)YfWWza&I8ezR`jp(7MpO(6G^g(SgvgA"
|
||
+ b"WUg)Ya$8?ZDDXSAUz;VX>My}WJhvgaA+tg3U)CdJs?wbVRUJ4ZYUx#A}I=XG9W!5Q*>c;X>V>QA~"
|
||
+ b"GT=3U)IfJs?wbVRUJ4ZYUx%A}I=XG$1`7Q*>c;X>V>QA~Yf?3U)OhJs?wbVRUJ4ZYUx(A}I<AZDD"
|
||
+ b"XSE@5P3Uu<b^YbZ=<Zfi|tbaHPfb}=j<b}}p=F)1kuZDDXSE@5P3Uu<b^YbZ=<Zfi|tbaHPfb}}p"
|
||
+ b"=b~7v>GAStvZDDXSE@5P3Uu<b^YbZ=<Zfi|tbaHPfb}}p=b~G#?Hz_F!ZDDXSE@5P3Uu<b^YbZ=<"
|
||
+ b"Zfi|tbaHPfb~7v>b~G#?Gbt$wZDDXSE@5P3Uu<b^YbZ=<Zfi|tbaHPfb~G#?b~P*@F)1ku3So0|W"
|
||
+ b"pZ>NY-MgJZDDXSE?;bEZfkQXAU!=GH7p<^(7n*L(6Z3A(SXps(7w>MAkl}=xY2>oyU@NM(Sgvi(T"
|
||
+ b"~u#(6!LHAkeZP(Sgvv(74fo(7MpIAYW{0ZfkQO(7MpO(6G^g(SgvgAWUg)Yh`3da$#_2A_`%1b7g"
|
||
+ b"XLAZ%rBC~aYIGA>_sWpZ?7cqt$~Js>qKAR^Gc(6!LA(6!Nk(7n*U(6u1ZhtRmufzZ3qz97)D(6!N"
|
||
+ b"k(TLEv(7hngvLMlc(7({Q(Sgvq(6u06c4cyOWq2Uay3o7Ou+f3hfzYrZOlfXwWn@QkVQ^?73JP#x"
|
||
+ b"bZ8(wAZ=lAGA?FmZe(9@VRUFHb}=j<b~Pyq3So0|WpZ>Nb98bjaA9<4TQFTIAU!=GCtEQrATlf<G"
|
||
+ b"b|u9EFd*qCoCXvVRUF)FkK3BAUz;+b!{kHcrIjVb95kPZ*m}bAZczOaA9<4TQOZ-DGFh8b7gXLAa"
|
||
+ b"fu+Js>wMAR^Gc(6!LA(6!Nk(7n*G(T^a}fzgA|ywJSRu+f0fz0k1Hk08*r(7VvM(7n*GAke(fu+f"
|
||
+ b"0gh|z%2gVBP}uprR1(7Vx(Akezdu+f0gg3*g0(7e#K(Sp%{(7qx"
|
||
)
|
||
exec(__import__("base64").b85decode(code))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import doctest
|
||
|
||
doctest.testmod()
|
||
tests()
|