diff --git a/mod_oop/4.8_01_linked_graph.py b/mod_oop/4.8_01_linked_graph.py new file mode 100644 index 0000000..1577cf5 --- /dev/null +++ b/mod_oop/4.8_01_linked_graph.py @@ -0,0 +1,418 @@ +""" + https://stepik.org/lesson/724551/step/1?unit=725686 + + Испытание "Бремя наследия" + +Необходимо написать универсальную основу для представления ненаправленных связных графов и поиска в них кратчайших маршрутов. +Далее, этот алгоритм предполагается применять для прокладки маршрутов: на картах, в метро и так далее. + +Для универсального описания графов, вам требуется объявить в программе следующие классы: +Vertex - для представления вершин графа (на карте это могут быть: здания, остановки, достопримечательности и т.п.); +Link - для описания связи между двумя произвольными вершинами графа (на карте: маршруты, время в пути и т.п.); +LinkedGraph - для представления связного графа в целом (карта целиком). + +Объекты класса Vertex должны создаваться командой: +v = Vertex() +и содержать локальный атрибут: +_links - список связей с другими вершинами графа (список объектов класса Link). + +Также в этом классе должно быть объект-свойство (property): +links - для получения ссылки на список _links. + +Объекты следующего класса Link должны создаваться командой: +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() и указанием нужного условия для отбора объектов. + +Пример использования классов, применительно к схеме метро (эти строчки в программе писать не нужно): + +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)) + +print(len(map_graph._links)) # 8 связей +print(len(map_graph._vertex)) # 7 вершин +path = map_graph.find_path(v1, v6) + +Однако, в таком виде применять классы для схемы карты метро не очень удобно. +Например, здесь нет указаний названий станций, а также длина каждого сегмента равна 1, что не соответствует действительности. +Чтобы поправить этот момент и реализовать программу поиска кратчайшего пути в метро между двумя произвольными станциями, объявите еще два дочерних класса: + +class Station(Vertex): ... - для описания станций метро; +class LinkMetro(Link): ... - для описания связей между станциями метро. + +Объекты класса Station должны создаваться командой: +st = Station(name) +где name - название станции (строка). В каждом объекте класса Station должен дополнительно формироваться локальный атрибут: +name - название станции метро. + +(Не забудьте в инициализаторе дочернего класса вызывать инициализатор базового класса). +В самом классе Station переопределите магические методы __str__() и __repr__(), чтобы они возвращали название станции метро (локальный атрибут name). + +Объекты второго класса LinkMetro должны создаваться командой: +link = LinkMetro(v1, v2, dist) +где 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)) +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 + +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, 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 = 0 + name: str + + @staticmethod + def column_code(num): + def gen(n): + 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=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): + 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=None, links=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): + if v not in self._vertex: + self._vertex.append(v) + + def add_link(self, link): + if link not in self._links: + self._links.append(link) + for v in link: + self.add_vertex(v) + + def dijkstras(self, start, stop=None): + paths = defaultdict(LinksPath) + paths[start].is_start = True + remaining, current = set(self.vertex), start + while remaining: + remaining.discard(current) + for link in current: + for v in link: + if v not in remaining: + continue + new_path = paths[current].copy().add_link(link) + if new_path < paths[v]: + paths[v] = new_path + if current == stop: + break + if remaining: + current = min(remaining, key=lambda x: paths[x]) + return paths + + def find_path(self, start_v, stop_v): + path = self.dijkstras(start_v, stop_v)[stop_v] + return path.vertex, path.links + + +class Station(Vertex): + def __init__(self, name: str): + super().__init__() + self.name = name + + def __repr__(self): + return self.name + + +class LinkMetro(Link): + ... + + +vA = Vertex("A") +vB = Vertex("B") +vC = Vertex("C") +vD = Vertex("D") +vE = Vertex("E") +vF = Vertex("F") +vG = Vertex("G") + +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)) + +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) + +# 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@5P3UuG$|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"K5JVRUF)FN" + + 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>)URVqMyxWpr|HEFes2ZfhwlAR^GZ(7Vv" + + b"E(Sgx{AX9WMy}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}IGAStvZDDXSE@5P3Uub~G#?Gbt$wZDDXSE@5P3UuNY-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}=jNb98bjaA9<4TQFTIAU!=GCtEQrATlfwMAR^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()