py_stepik/mod_oop/4.8_01_linked_graph.py

425 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()