From 81501062a978f0d2eb63f45197d89047426e8087 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bertrand=20N=C3=A9ron?= <freeh4cker@gmail.com>
Date: Fri, 6 Sep 2019 17:11:41 +0200
Subject: [PATCH] :sparkles: add support to direted graph and weighted graph

new data structure to support weigthed graph
add 2 concrete implementaion of graph directed and undirected
---
 Graph/graph/graph_3.py    | 350 +++++++++++++++++++-------------------
 Graph/graph/traversing.py |  67 ++++++++
 2 files changed, 241 insertions(+), 176 deletions(-)
 create mode 100644 Graph/graph/traversing.py

diff --git a/Graph/graph/graph_3.py b/Graph/graph/graph_3.py
index 86f4494..ea05577 100644
--- a/Graph/graph/graph_3.py
+++ b/Graph/graph/graph_3.py
@@ -1,37 +1,67 @@
+from typing import Iterator, Dict
 import itertools
-from abc import ABCMeta
-from typing import Iterator, Dict, Set, Sequence, Union, Tuple
+from abc import ABCMeta, abstractmethod
 
 
-class Graph(metaclass=ABCMeta):
+class Node:
+    """
+    Graph node representation
+    """
 
-    def __init__(self):
-        self.nodes: set[Node] = set()
-        self.vertices: Dict[Node: set[Node]]
+    _id = itertools.count(0)
 
-    def add_node(self):
-        pass
+    def __init__(self, **kwargs) -> None:
+        self.id: int = next(self._id)
+        for k, v in kwargs:
+            setattr(self, k, v)
 
-    def del_node(self):
-        pass
+
+    def __hash__(self) -> int:
+        # to be usable in set an object must be hashable
+        # so we need to implement __hash__
+        # which must return an int unique per object
+        # here we have the Node identifier
+        return self.id
+
+    def __str__(self) -> str:
+        return f"node_{self.id}"
+
+    def __repr__(self) -> str:
+        return str(self)
 
 
-class NDGraph:
+class Edge:
     """
-    To handle Non Directed Graph
-    A graph is a collection of Nodes linked by edges
-    there are basically to way to implement a graph
-     - The graph handles Nodes, each nodes know it's neighbors
-     - The graph handles nodes and the edges between nodes
-     below I implemented the 2nd version.
+    modelize edge between two nodes, each edge can support properties
     """
 
-    def __init__(self, nodes: Sequence['Node']) -> None:
-        self.nodes: Set['Node'] = {n for n in nodes}
-        self.vertices: Dict['Node': Set['Node']] = {n: set() for n in self.nodes}
+    def __init__(self, src: Node, target: Node, directed=True, **kwargs) -> None:
+        self.src = src
+        self.target = target
+        self.directed = directed
+        for k, v in kwargs.items():
+            setattr(self, k, v)
 
+    def __str__(self):
+        """string representation of this edge"""
+        return f"{self.src} -{'>' if self.directed else ''} {self.target}"
 
-    def add_node(self, node: 'Node') -> None:
+
+    def properties(self) -> Dict:
+        """the edge properties"""
+        return {k: v for k, v in self.__dict__.items()}
+
+
+class Graph(metaclass=ABCMeta):
+    """
+    Base class for graphs.
+    A graph is a collection of Nodes linked by edges
+    """
+
+    def __init__(self) -> None:
+        self._nodes: {}
+
+    def add_node(self, node) -> None:
         """
         Add a node to the graph
 
@@ -39,12 +69,11 @@ class NDGraph:
         :type node: :class:`Node`
         :return: None
         """
-        self.nodes.add(node)
-        if node not in self.vertices:
-            self.vertices[node] = set()
+        if node not in self._nodes:
+            self._nodes[node] = {}
 
 
-    def del_node(self, node: 'Node') -> None:
+    def del_node(self, node: Node) -> None:
         """
         Remove Node from Graph
 
@@ -52,33 +81,12 @@ class NDGraph:
         :type node: :class:`Node`
         :return: None
         """
-        self.nodes.remove(node)
-        neighbors = self.vertices[node]
-        for nbh in neighbors:
-            self.vertices[nbh].remove(node)
-        del self.vertices[node]
+        for nbh in self.neighbors(node):
+            del self._nodes[nbh][node]
+        del self._nodes[node]
 
 
-    def add_edge(self, node_1: 'Node', nodes: Sequence['Node']):
-        """
-        Add vertice between node1 and all nodes in nodes
-
-        :param node_1: the reference node
-        :type node_1: :class:`Node`
-        :param nodes: the nodes connected to node_1
-        :type nodes: Sequence of :class:`Node`
-        :return: Node
-        """
-        if node_1 not in self.nodes:
-            raise ValueError("the node_1 must be in Graph")
-        for n in nodes:
-            if n not in self.nodes:
-                raise ValueError("node must be add to Graph before creating edge")
-            self.vertices[node_1].add(n)
-            self.vertices[n].add(node_1)
-
-
-    def neighbors(self, node: 'Node') -> Set['Node']:
+    def neighbors(self, node: Node) -> Iterator[Node]:
         """
         return the nodes connected to node
 
@@ -86,174 +94,164 @@ class NDGraph:
         :type node: :class:`Node`
         :return: a set of :class:`Node`
         """
-        return {n for n in self.vertices[node]}
+        for n in self._nodes[node]:
+            yield n
 
 
-    def get_weight(self, node_1, node_2):
+    def nodes(self) -> Iterator[Node]:
         """
+        Iterates over all nodes belonging to the graph
 
-        :param node_1:
-        :param node_2:
-        :return:
+        :return: iterator
         """
-        return 1
+        for n in self._nodes:
+            yield n
 
+    def edges(self, node: Node) -> Iterator[Edge]:
+        """
+        Iterates over edge connecting to/from this node
+        :param node:
+        :return:
+        """
+        for e in self._nodes[node].values():
+            yield e
 
-class Node:
+    def has_edge(self, edge) -> bool:
+        """
+        :param edge:
+        :return: True if it exists an edge connecting edge nodes src and target
+        """
+        return edge.src in self._nodes and edge.target in self._nodes[edge.src]
 
-    _id = itertools.count(0)
+    @abstractmethod
+    def add_edge(self, src: Node, target: Node, **edge_prop) -> None:
+        """
 
-    def __init__(self) -> None:
-        self.id: int = next(self._id)
+        :param edge:
+        :return:
+        """
+        pass
 
-    def __hash__(self) -> int:
-        # to be usable in set an object must be hashable
-        # so we need to implement __hash__
-        # which must return an int unique per object
-        # here we ad the identifier of the Node
-        return self.id
+    @abstractmethod
+    def del_edge(self, src: Node, target: Node) -> None:
+        """
+        remove the edge between nodes src and target
+        :param src:
+        :param target:
+        :return:
+        """
+        pass
 
-    def __str__(self):
-        return f"node_{self.id}"
 
-    def __repr__(self):
-        return str(self)
+    @staticmethod
+    @abstractmethod
+    def is_directed() -> bool:
+        pass
 
 
-class Edge:
+    def order(self):
+        """
+        The Order of a graph is the number of Nodes in the graph
+        :return: 
+        """
+        return len(self._nodes)
 
-    def __init__(self, src, target, weight):
-        self.src = src
-        self.target = target
-        self.weight = weight
+    @abstractmethod
+    def size(self):
+        """
+        The size of a graph is the number of edges in the graph.
+        :return:
+        """
+        size = 0
+        for n in self.nodes():
+            size += len(self._nodes[n])
+        return size
 
 
-class NDWGraph:
+class UnDirectedGraph(Graph):
     """
-    To handle Non Directed Graph
+    To handle UnDirected Graph
     A graph is a collection of Nodes linked by edges
-    there are basically to way to implement a graph
-     - The graph handles Nodes, each nodes know it's neighbors
-     - The graph handles nodes and the edges between nodes
-     below I implemented the 2nd version.
     """
 
-    def __init__(self, node) -> None:
-        self.nodes: Set[Node] = {}
+    def __init__(self) -> None:
+        self._nodes = {}
 
+    @staticmethod
+    def is_directed() -> bool:
+        return False
 
-    def add_node(self, node: Node) -> None:
+    def size(self) -> int:
         """
-        Add a node to the graph
-
-        :param node: the node to add
-        :type node: :class:`Node`
-        :return: None
+        :return: The number of edges of this graph
         """
-        if node not in self.nodes:
-            self.nodes[node] = {}
+        return super().size() // 2
 
 
-    def del_node(self, node: Node) -> None:
+    def add_edge(self, node_1, node_2, **prop):
         """
-        Remove Node from Graph
+        Add vertex between node1 and all nodes in nodes
 
-        :param node: the node to add
-        :type node: :class:`Node`
-        :return: None
+        :return: Node
         """
-        neighbors = self.nodes[node]
-        for nbh in neighbors:
-            del self.vertices[nbh][node]
-        del self.nodes[node]
+        self.add_node(node_1)
+        self.add_node(node_2)
+        self._nodes[node_1][node_2] = Edge(node_1, node_2, directed=False, **prop)
+        self._nodes[node_2][node_1] = Edge(node_2, node_1, directed=False, **prop)
 
 
-    def add_edge(self, node_1: Node, node_2: Node, weight: Union[int, float]):
+    def del_edge(self, src: Node, target: Node) -> None:
         """
-        Add vertex between node1 and all nodes in nodes
-
-        :param node_1: the reference node
-        :param node_2: the nodes connected to node_1
-        :param weight: the weight of the edge between node_1 and node_2
-        :return: Node
+        remove edge between nodes src and target
+        :param src:
+        :param target:
+        :return:
         """
-        for n in node_1, node_2:
-            if n not in self.nodes:
-                raise ValueError(f"node {n.id} not found in Graph. The node must be in Graph.")
-        self.nodes[node_1][node_2] = weight
-        self.nodes[node_2][node_1] = weight
+        del self._nodes[src][target]
+        del self._nodes[target][src]
 
 
-    def neighbors(self, node):
-        """
-        return the nodes connected to node
+class DirectedGraph(Graph):
+    """
+    To handle Directed Graph
+    A graph is a collection of Nodes linked by edges
+    """
 
-        :param node: the reference node
-        :type node: :class:`Node`
-        :return: a set of :class:`Node`
-        """
-        return {n for n, w in self.nodes[node].items()}
+    def __init__(self) -> None:
+        self._nodes = {}
 
 
-    def get_weight(self, node_1, node_2):
-        """
+    @staticmethod
+    def is_directed() -> bool:
+        return True
 
-        :param node_1:
-        :param node_2:
-        :return:
+
+    def size(self):
         """
-        return self.nodes[node_1][node_2]
+        :return: The number of edges of this graph
+        """
+        return super().size()
 
 
-def DFS(graph: NDGraph, node: Node) -> Iterator[Node]:
-    """
-    **D**epth **F**irst **S**earch.
-    We start the path from the node given as argument,
-    This node is labeled as 'visited'
-    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
-    We pick the last element to visit and visit it
-    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit').
-    on so on until there no nodes to visit anymore.
-
-    :param graph:
-    :param node:
-    :return:
-    """
-    to_visit = [node]
-    visited = set()
-    while to_visit:
-        n = to_visit.pop(-1)
-        visited.add(n)
-        new_to_visit = graph.neighbors(n) - visited - set(to_visit)
-        to_visit.extend(new_to_visit)
-        yield n
+    def add_edge(self, src: Node, target: Node, **prop) -> None:
+        """
+        Add edge from node *src* to node *target*
+
+        :param src: the src node
+        :param target: the target node
+        :param prop: the edge properties
+        :return: None
+        """
+        self.add_node(src)
+        self.add_node(target)
+        self._nodes[src][target] = Edge(src, target, directed=True, **prop)
 
 
-def BFS(graph: NDGraph, node: Node) -> Iterator[Node]:
-    """
-    **B**readth **F**irst **s**earch
-    We start the path from the node given as argument,
-    This node is labeled as 'visited'
-    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
-    we pick the **first** element of the nodes to visit and visit it.
-    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit')
-    on so on until there no nodes to visit anymore.
-
-    :param graph:
-    :param node:
-    :return:
-    """
-    to_visit = [node]
-    visited = set()
-    parent = None
-    while to_visit:
-        n = to_visit.pop(0)
-        visited.add(n)
-        new_to_visit = graph.neighbors(n) - visited - set(to_visit)
-        to_visit.extend(new_to_visit)
-        if not parent:
-            weight = 0
-        else:
-            weight = graph.get_weight(parent, n)
-        parent = n
-        yield n, weight
+    def del_edge(self, src: Node, target: Node) -> Node:
+        """
+        Remove edge between nodes src and target
+        :param src:
+        :param target:
+        :return:
+        """
+        del self._nodes[src][target]
diff --git a/Graph/graph/traversing.py b/Graph/graph/traversing.py
new file mode 100644
index 0000000..07f7da4
--- /dev/null
+++ b/Graph/graph/traversing.py
@@ -0,0 +1,67 @@
+from typing import Iterator, List, Union, Tuple
+
+from .helpers import LIFO, FIFO
+from .graph_3 import Node, Edge, Graph
+
+
+def _traversing(to_visit: Union[FIFO, LIFO], graph: Graph, node: Node) -> Iterator[Tuple[Node, List[Edge]]]:
+    """
+    function that traverse the graph starting from node
+
+    :param to_visit:
+    :param graph:
+    :param node:
+    :return: an iterator at each step return the visiting node and the path
+    """
+    to_visit.add(node)
+    visited = set()
+    parent = {}
+    path = []
+    while to_visit:
+        node = to_visit.pop()
+        # it is important to add node in visited right now
+        # and not a the end of the block
+        # otherwise node is not anymore in to_visit and not yet in visited
+        # so when we explore neighbors we add it again in to_visit
+        visited.add(node)
+        for edge in graph.edges(node):
+            if edge.target not in visited and edge.target not in to_visit:
+                parent[edge.target] = edge
+                to_visit.add(edge.target)
+        if node in parent:
+            path.append(parent[node])
+        yield node, path
+
+
+def DFS(graph: Graph, node: Node) -> Iterator[Tuple[Node, List[Edge]]]:
+    """
+    **D**\ epth **F**\ irst **S**\ earch.
+    We start the path from the node given as argument,
+    This node is labeled as 'visited'
+    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
+    We pick the last element to visit and visit it
+    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit').
+    on so on until there no nodes to visit anymore.
+
+    :param graph: The graph to traverse
+    :param node: The starting node
+    :return: iterator on nodes
+    """
+    return _traversing(FIFO(), graph, node)
+
+
+def BFS(graph: Graph, node: Node) -> Iterator[Tuple[Node, List[Edge]]]:
+    """
+    **B**\ readth **F**\ irst **s**\ earch
+    We start the path from the node given as argument,
+    This node is labeled as 'visited'
+    The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit'
+    we pick the **first** element of the nodes to visit and visit it.
+    (The neighbors of this node which have not been already 'visited' nor 'to visit' are labelled as 'to visit')
+    on so on until there no nodes to visit anymore.
+
+    :param graph:
+    :param node:
+    :return:
+    """
+    return _traversing(LIFO(), graph, node)
-- 
GitLab