py_stepik/mod_oop/4.8_01_linked_graph.py

425 lines
20 KiB
Python
Raw Normal View History

2024-04-24 14:14:54 +00:00
"""
https://stepik.org/lesson/724551/step/1?unit=725686
Испытание "Бремя наследия"
Необходимо написать универсальную основу для представления ненаправленных связных графов и поиска в них кратчайших маршрутов.
Далее, этот алгоритм предполагается применять для прокладки маршрутов: на картах, в метро и так далее.
Для универсального описания графов, вам требуется объявить в программе следующие классы:
Vertex - для представления вершин графа (на карте это могут быть: здания, остановки, достопримечательности и т.п.);
Link - для описания связи между двумя произвольными вершинами графа (на карте: маршруты, время в пути и т.п.);
LinkedGraph - для представления связного графа в целом (карта целиком).
Объекты класса Vertex должны создаваться командой:
2024-04-25 07:43:01 +00:00
>>> v = Vertex()
2024-04-24 14:14:54 +00:00
и содержать локальный атрибут:
_links - список связей с другими вершинами графа (список объектов класса Link).
Также в этом классе должно быть объект-свойство (property):
links - для получения ссылки на список _links.
Объекты следующего класса Link должны создаваться командой:
2024-04-25 07:43:01 +00:00
>>> v1 = Vertex(); v2 = Vertex()
>>> link = Link(v1, v2)
2024-04-24 14:14:54 +00:00
где v1, v2 - объекты класса Vertex (вершины графа). Внутри каждого объекта класса Link должны формироваться следующие локальные атрибуты:
_v1, _v2 - ссылки на объекты класса Vertex, которые соединяются данной связью;
_dist - длина связи (по умолчанию 1); это может быть длина пути, время в пути и др.
В классе Link должны быть объявлены следующие объекты-свойства:
v1 - для получения ссылки на вершину v1;
v2 - для получения ссылки на вершину v2;
dist - для изменения и считывания значения атрибута _dist.
Наконец, объекты третьего класса LinkedGraph должны создаваться командой:
2024-04-25 07:43:01 +00:00
>>> map_graph = LinkedGraph()
2024-04-24 14:14:54 +00:00
В каждом объекте класса 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() и указанием нужного условия для отбора объектов.
Пример использования классов, применительно к схеме метро (эти строчки в программе писать не нужно):
2024-04-25 07:43:01 +00:00
>>> 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)])
2024-04-24 14:14:54 +00:00
Однако, в таком виде применять классы для схемы карты метро не очень удобно.
Например, здесь нет указаний названий станций, а также длина каждого сегмента равна 1, что не соответствует действительности.
Чтобы поправить этот момент и реализовать программу поиска кратчайшего пути в метро между двумя произвольными станциями, объявите еще два дочерних класса:
class Station(Vertex): ... - для описания станций метро;
class LinkMetro(Link): ... - для описания связей между станциями метро.
Объекты класса Station должны создаваться командой:
2024-04-25 07:43:01 +00:00
>>> st = Station(name := "Домодедовская")
2024-04-24 14:14:54 +00:00
где name - название станции (строка). В каждом объекте класса Station должен дополнительно формироваться локальный атрибут:
name - название станции метро.
(Не забудьте в инициализаторе дочернего класса вызывать инициализатор базового класса).
В самом классе Station переопределите магические методы __str__() и __repr__(), чтобы они возвращали название станции метро (локальный атрибут name).
Объекты второго класса LinkMetro должны создаваться командой:
2024-04-25 07:43:01 +00:00
>>> link = LinkMetro(v1, v2, dist := 2)
2024-04-24 14:14:54 +00:00
где v1, v2 - вершины (станции метро); dist - расстояние между станциями (любое положительное число).
(Также не забывайте в инициализаторе этого дочернего класса вызывать инициализатор базового класса).
В результате, эти классы должны совместно работать следующим образом (эти строчки в программе писать не нужно):
2024-04-25 07:43:01 +00:00
>>> 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
2024-04-24 14:14:54 +00:00
P.S. В программе нужно объявить только классы Vertex, Link, LinkedGraph, Station, LinkMetro. На экран ничего выводить не нужно.
"""
from collections import defaultdict
from dataclasses import dataclass, field
from functools import total_ordering
2024-04-25 07:43:01 +00:00
from typing import List, Optional, Tuple, Union
2024-04-24 14:14:54 +00:00
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:
2024-04-25 07:43:01 +00:00
_num: int = 0
2024-04-24 14:14:54 +00:00
name: str
@staticmethod
2024-04-25 07:43:01 +00:00
def column_code(num: int) -> str:
"""Имя как код колонки в Excel по порядковому номеру с 1"""
def gen(n: int):
2024-04-24 14:14:54 +00:00
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):
2024-04-25 07:43:01 +00:00
def __init__(self, name: Optional[str] = None):
2024-04-24 14:14:54 +00:00
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")
2024-04-25 07:43:01 +00:00
def add_link(self, link: Link):
2024-04-24 14:14:54 +00:00
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:
2024-04-25 07:43:01 +00:00
def __init__(self, vertex: Optional[Vertex] = None, links: Optional[Link] = None):
2024-04-24 14:14:54 +00:00
self._vertex = vertex or []
self._links = links or []
def __repr__(self) -> str:
return f"{self.__class__.__name__}{(self.vertex, self.links)!r}"
2024-04-25 07:43:01 +00:00
def add_vertex(self, v: Vertex):
2024-04-24 14:14:54 +00:00
if v not in self._vertex:
self._vertex.append(v)
2024-04-25 07:43:01 +00:00
def add_link(self, link: Link):
2024-04-24 14:14:54 +00:00
if link not in self._links:
self._links.append(link)
for v in link:
self.add_vertex(v)
2024-04-25 07:43:01 +00:00
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])
2024-04-24 14:14:54 +00:00
paths = defaultdict(LinksPath)
paths[start].is_start = True
2024-04-25 07:43:01 +00:00
remaining = set(self.vertex)
for current in walk(remaining, paths, start):
2024-04-24 14:14:54 +00:00
for link in current:
2024-04-25 07:43:01 +00:00
for v in filter(lambda v: v in remaining, link):
2024-04-24 14:14:54 +00:00
new_path = paths[current].copy().add_link(link)
2024-04-25 07:43:01 +00:00
paths[v] = min(paths[v], new_path)
2024-04-24 14:14:54 +00:00
return paths
2024-04-25 07:43:01 +00:00
def find_path(
self, start_v: Vertex, stop_v: Vertex
) -> Tuple[List[Vertex], List[Link]]:
2024-04-24 14:14:54 +00:00
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):
...
2024-04-25 07:43:01 +00:00
print("-------------------------------------------")
2024-04-24 14:14:54 +00:00
2024-04-25 07:43:01 +00:00
vA, vB, vC, vD, vE, vF, vG = [Vertex() for _ in range(7)]
2024-04-24 14:14:54 +00:00
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))
2024-04-25 07:43:01 +00:00
# ([Vertex('A'), Vertex('B'), Vertex('G')], [Link(Vertex('A'), Vertex('B'), 1), Link(Vertex('B'), Vertex('G'), 1)])
2024-04-24 14:14:54 +00:00
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)
2024-04-25 07:43:01 +00:00
Vertex._num = 0 # naming reset
2024-04-24 14:14:54 +00:00
# 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()