diff --git a/DIRECTORY.md b/DIRECTORY.md index 6c16c150..6b04b103 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -80,6 +80,8 @@ * Graphs * Course Schedule * [Test Course Schedule](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/course_schedule/test_course_schedule.py) + * Frog Position After T Seconds + * [Test Frog Position After T Seconds](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/frog_position_after_t_seconds/test_frog_position_after_t_seconds.py) * Greedy * Min Arrows * [Test Find Min Arrows](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/min_arrows/test_find_min_arrows.py) @@ -209,7 +211,19 @@ * [Default Dicts](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/dicts/default_dicts.py) * [Ordered Dict](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/dicts/ordered_dict.py) * Graphs - * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge.py) + * Edge + * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py) + * [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py) + * [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py) + * [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py) + * [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py) + * [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py) + * [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py) + * [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py) + * Undirected + * [Test Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/test_utils.py) + * [Undirected Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/undirected_graph.py) + * [Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/utils.py) * [Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/vertex.py) * Hashmap * [Test Hashmap](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/hashmap/test_hashmap.py) @@ -288,8 +302,10 @@ * [Test Binary Search Tree Search](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_search.py) * [Test Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/test_utils.py) * Tree + * [Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/binary_tree.py) * [Test Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree.py) * [Test Binary Tree Deserialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_deserialize.py) + * [Test Binary Tree Min Camera Cover](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_min_camera_cover.py) * [Test Binary Tree Serialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_serialize.py) * [Test Binary Tree Visible Nodes](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_visible_nodes.py) * [Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/utils.py) diff --git a/datastructures/graphs/__init__.py b/datastructures/graphs/__init__.py index 21360257..1e928568 100755 --- a/datastructures/graphs/__init__.py +++ b/datastructures/graphs/__init__.py @@ -1,230 +1,14 @@ -from abc import ABC, abstractmethod -from collections import defaultdict -from pprint import PrettyPrinter -from typing import List, Set, Union, Generic, TypeVar -from datastructures.stacks import Stack from .vertex import Vertex -from .edge import Edge - -T = TypeVar("T") - - -class Graph(ABC, Generic[T]): - """ - Represents a Graph Data structure - """ - - def __init__(self, edge_list: List[Edge] = None): - if edge_list is None: - edge_list = [] - self.edge_list = edge_list - self.adjacency_list = defaultdict(List[Vertex]) - self.__construct_adjacency_list() - self.nodes = [] - self.node_count = len(self.nodes) - - def add(self, node_one: Vertex, node_two: Vertex): - """ - Adds a connection between node_one and node_two - """ - node_one.neighbours.append(node_two) - node_two.neighbours.append(node_one) - edge = Edge(source=node_one, destination=node_two) - self.edge_list.append(edge) - self.adjacency_list[node_one].append(node_two) - self.adjacency_list[node_two].append(node_one) - self.nodes.append(node_one) - self.nodes.append(node_two) - - def __construct_adjacency_list(self): - """ - Construct adjacency list - """ - for edge in self.edge_list: - self.adjacency_list[edge.source].append(edge.destination) - - @abstractmethod - def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]: - """ - Given the root node to traverse and a target node, returns the BFS result of this Graph from the root node to - the target node - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def bfs_from_node(self, source: Vertex) -> Set[Vertex]: - """ - Given the source to traverse, returns the BFS result of this Graph from the source node - """ - raise NotImplementedError("Not yet implemented") - - def topological_sorted_order(self) -> List[Vertex]: - """ - Returns the topological sorted order of the Graph - """ - # These static variables are used to perform DFS recursion - # white nodes depict nodes that have not been visited yet - # gray nodes depict ongoing recursion - # black nodes depict recursion is complete - # An edge leading to a BLACK node is not a "cycle" - white = 1 - gray = 2 - black = 3 - - # Nothing to do here - if self.node_count == 0: - return [] - - is_possible = True - stack = Stack() - - # By default all nodes are WHITE - visited_nodes = {node: white for node in range(self.node_count)} - - def dfs(node: Vertex): - nonlocal is_possible - - # Don't recurse further if we found a cycle already - if not is_possible: - return - - # start recursion - visited_nodes[node] = gray - - # Traverse on neighbouring nodes/vertices - if node in self.adjacency_list: - for neighbour in self.adjacency_list[node]: - if visited_nodes[neighbour] == white: - dfs(node) - elif visited_nodes[node] == gray: - # An Edge to a Gray vertex/node represents a cycle - is_possible = False - - # Recursion ends. We mark if as BLACK - visited_nodes[node] = black - stack.push(node) - - for node in self.nodes: - # if the node is unprocessed, then call DFS on it - if visited_nodes[node] == white: - dfs(node) - - return list(stack.stack) if is_possible else [] - - def print(self): - pretty_print = PrettyPrinter() - pretty_print.pprint(self.adjacency_list) - - def remove(self, node: Vertex) -> None: - """ - Removes all references to a node - :param node - """ - for _, cxns in self.adjacency_list.items(): - try: - cxns.remove(node) - except KeyError: - pass - - try: - del self.adjacency_list[node] - except KeyError: - pass - - def is_connected(self, node_one: Vertex, node_two: Vertex) -> bool: - return ( - node_one in self.adjacency_list - and node_two in self.adjacency_list[node_two] - ) - - def find_path( - self, node_one: Vertex, node_two: Vertex, path=None - ) -> Union[List, None]: - """ - Find any path between node_one and node_two. May not be the shortest path - :param node_one - :param node_two - :param path - """ - - if path is None: - path = [] - path = [path] + [node_one] - - if node_one.data == node_two.data: - return path - - if node_one.data not in self.adjacency_list: - return None - - for node in self.adjacency_list[node_one]: - if node.data not in path: - new_path = self.find_path(node, node_two, path) - - if new_path: - return new_path - - return None - - def find_all_paths( - self, node_one: Vertex, node_two: Vertex, path: List = None - ) -> list: - """ - Finds all paths between node_one and node_two, where node_one is the start & node_two is the end - :param node_one Graph Node - :param node_two Graph Node - :param path - """ - if path is None: - path = [] - path = path + [node_one] - - if node_one.data == node_two.data: - return [path] - - if node_one.data not in self.adjacency_list: - return [] - - paths = [] - - for node in self.adjacency_list[node_one.data]: - if node not in path: - newpaths = self.find_all_paths(Vertex(node), node_two, path) - for newpath in newpaths: - paths.append(newpath) - - return paths - - def find_shortest_path( - self, node_one: Vertex, node_two: Vertex, path: List = None - ) -> Union[List, None]: - """ - Finds the shortest path between 2 nodes in the graph - """ - if path is None: - path = [] - - path = path + [node_one] - - if node_one.data == node_two.data: - return path - - if node_one.data not in self.adjacency_list: - return None - - shortest = None - - for node in self.adjacency_list[node_one]: - if node.data not in path: - newpath = self.find_shortest_path(node, node_two, path) - if newpath: - if not shortest or len(newpath) < len(shortest): - shortest = newpath - - return shortest - - def __str__(self): - """ - Return string representation of this Graph - """ - return f"Graph: {self.adjacency_list}" +from .edge import Edge, EdgeType, DirectedEdge, UndirectedEdge, SelfEdge, HyperEdge +from .graph import Graph + +__all__ = [ + "Edge", + "EdgeType", + "Vertex", + "Graph", + "UndirectedEdge", + "DirectedEdge", + "SelfEdge", + "HyperEdge", +] diff --git a/datastructures/graphs/edge.py b/datastructures/graphs/edge.py deleted file mode 100644 index 405062c8..00000000 --- a/datastructures/graphs/edge.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import AnyStr, Optional, Union, Dict, Any -from enum import Enum, unique -from uuid import uuid4 -from .vertex import Vertex - - -@unique -class EdgeType(Enum): - UNDIRECTED = 1 - DIRECTED = 2 - SELF_DIRECTED = 3 - SELF_UNDIRECTED = 4 - HYPER_DIRECTED = 5 - HYPER_UNDIRECTED = 6 - - -class Edge: - """ - Edge representation of an Edge in a Graph - """ - - def __init__( - self, - source: Vertex, - destination: Vertex, - weight: Optional[Union[int, float]] = None, - edge_type: EdgeType = EdgeType.UNDIRECTED, - relationship_type: Optional[AnyStr] = None, - properties: Optional[Dict[str, Any]] = None, - identifier: AnyStr = uuid4(), - ): - self.id = identifier - self.source = source - self.destination = destination - self.weight = weight - self.type = edge_type - self.properties = properties - self.relationship_type = relationship_type - - self.__validate_edge() - - def __validate_edge(self): - if self.type == EdgeType.SELF_DIRECTED or self.type == EdgeType.SELF_UNDIRECTED: - if self.destination.id != self.source.id: - raise ValueError( - f"Edge denoted as {self.type} but source node {self.source} & " - f"destination node {self.destination} are not the same" - ) - - def __str__(self): - return ( - f"Id: {self.id}, Source: {self.source}, Destination: {self.destination}, Weight: {self.weight}, " - f"Properties: {self.properties}, RelationshipType: {self.relationship_type}" - ) diff --git a/datastructures/graphs/edge/__init__.py b/datastructures/graphs/edge/__init__.py new file mode 100644 index 00000000..17257a08 --- /dev/null +++ b/datastructures/graphs/edge/__init__.py @@ -0,0 +1,15 @@ +from .edge import Edge +from .edge_type import EdgeType +from .edge_undirected import UndirectedEdge +from .edge_directed import DirectedEdge +from .edge_self import SelfEdge +from .edge_hyper import HyperEdge + +__all__ = [ + "Edge", + "EdgeType", + "UndirectedEdge", + "DirectedEdge", + "SelfEdge", + "HyperEdge", +] diff --git a/datastructures/graphs/edge/edge.py b/datastructures/graphs/edge/edge.py new file mode 100644 index 00000000..760dd60c --- /dev/null +++ b/datastructures/graphs/edge/edge.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod +from typing import AnyStr, Union +from .edge_type import EdgeType +from typing import Any, Dict, Optional, Generic, TypeVar, List +from uuid import uuid4 + +T = TypeVar("T") + + +class Edge(ABC, Generic[T]): + """ + Edge representation of an abstract Edge in a Graph + """ + + def __init__( + self, + weight: Optional[Union[int, float]] = None, + properties: Optional[Dict[str, Any]] = None, + identifier: AnyStr = uuid4(), + ): + self.id = identifier + self.weight = weight + self.properties = properties + + def __str__(self): + return f"Id: {self.id}, Weight: {self.weight}, Properties: {self.properties}" + + @abstractmethod + def edge_type(self) -> EdgeType: + raise NotImplementedError("Not implemented") + + def is_unweighted(self) -> bool: + return self.weight is None + + @abstractmethod + def vertices(self) -> List[Any]: + raise NotImplementedError("Not implemented") diff --git a/datastructures/graphs/edge/edge_directed.py b/datastructures/graphs/edge/edge_directed.py new file mode 100644 index 00000000..4ff609a4 --- /dev/null +++ b/datastructures/graphs/edge/edge_directed.py @@ -0,0 +1,34 @@ +from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any +from uuid import uuid4 +from .edge_type import EdgeType +from .edge import Edge + +T = TypeVar("T") + + +class DirectedEdge(Edge, Generic[T]): + """ + Directed Edge representation of a directed Edge in a Graph where the edge connects two vertices which has a source + vertex and a destination vertex. + """ + + def __init__( + self, + source: Any, + destination: Any, + weight: Optional[Union[int, float]] = None, + properties: Optional[Dict[str, Any]] = None, + identifier: AnyStr = uuid4(), + ): + super().__init__(weight, properties, identifier) + self.source = source + self.destination = destination + + def __str__(self): + return f"{super().__str__()}, Source: {self.source}, Destination: {self.destination}" + + def edge_type(self) -> EdgeType: + return EdgeType.DIRECTED + + def vertices(self) -> List[Any]: + return [self.source, self.destination] diff --git a/datastructures/graphs/edge/edge_hyper.py b/datastructures/graphs/edge/edge_hyper.py new file mode 100644 index 00000000..3347e4da --- /dev/null +++ b/datastructures/graphs/edge/edge_hyper.py @@ -0,0 +1,31 @@ +from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any +from uuid import uuid4 +from .edge_type import EdgeType +from .edge import Edge + +T = TypeVar("T") + + +class HyperEdge(Edge, Generic[T]): + """ + HyperEdge representation of a hyper-edge in a Graph where the edge connects to the multiple vertices + """ + + def __init__( + self, + nodes: List[Any], + weight: Optional[Union[int, float]] = None, + properties: Optional[Dict[str, Any]] = None, + identifier: AnyStr = uuid4(), + ): + super().__init__(weight, properties, identifier) + self.nodes = nodes + + def __str__(self): + return f"{super().__str__()}, Nodes: {self.nodes}" + + def edge_type(self) -> EdgeType: + return EdgeType.SELF + + def vertices(self) -> List[Any]: + return self.nodes diff --git a/datastructures/graphs/edge/edge_self.py b/datastructures/graphs/edge/edge_self.py new file mode 100644 index 00000000..3b69ff30 --- /dev/null +++ b/datastructures/graphs/edge/edge_self.py @@ -0,0 +1,31 @@ +from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any +from uuid import uuid4 +from .edge_type import EdgeType +from .edge import Edge + +T = TypeVar("T") + + +class SelfEdge(Edge, Generic[T]): + """ + Self-Edge representation of a self-edge in a Graph where the edge connects to the same vertex + """ + + def __init__( + self, + node: Any, + weight: Optional[Union[int, float]] = None, + properties: Optional[Dict[str, Any]] = None, + identifier: AnyStr = uuid4(), + ): + super().__init__(weight, properties, identifier) + self.node_one = node + + def __str__(self): + return f"{super().__str__()}, Node: {self.node_one}" + + def edge_type(self) -> EdgeType: + return EdgeType.SELF + + def vertices(self) -> List[Any]: + return [self.node_one] diff --git a/datastructures/graphs/edge/edge_type.py b/datastructures/graphs/edge/edge_type.py new file mode 100644 index 00000000..436fb065 --- /dev/null +++ b/datastructures/graphs/edge/edge_type.py @@ -0,0 +1,10 @@ +from enum import Enum, unique + + +@unique +class EdgeType(Enum): + UNDIRECTED = 1 + DIRECTED = 2 + SELF = 3 + HYPER_DIRECTED = 4 + HYPER_UNDIRECTED = 5 diff --git a/datastructures/graphs/edge/edge_undirected.py b/datastructures/graphs/edge/edge_undirected.py new file mode 100644 index 00000000..e11eac4c --- /dev/null +++ b/datastructures/graphs/edge/edge_undirected.py @@ -0,0 +1,35 @@ +from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any +from uuid import uuid4 +from .edge_type import EdgeType +from .edge import Edge + +T = TypeVar("T") + + +class UndirectedEdge(Edge, Generic[T]): + """ + Undirected Edge representation of an undirected Edge in a Graph where the edge connects two vertices. + """ + + def __init__( + self, + node_one: Any, + node_two: Any, + weight: Optional[Union[int, float]] = None, + properties: Optional[Dict[str, Any]] = None, + identifier: AnyStr = uuid4(), + ): + super().__init__(weight, properties, identifier) + self.node_one = node_one + self.node_two = node_two + + def __str__(self): + return ( + f"{super().__str__()}, NodeOne: {self.node_one}, NodeTwo: {self.node_two}" + ) + + def edge_type(self) -> EdgeType: + return EdgeType.Undirected + + def vertices(self) -> List[Any]: + return [self.node_one, self.node_two] diff --git a/datastructures/graphs/graph.py b/datastructures/graphs/graph.py new file mode 100644 index 00000000..7057e8da --- /dev/null +++ b/datastructures/graphs/graph.py @@ -0,0 +1,230 @@ +from abc import ABC, abstractmethod +from collections import defaultdict +from pprint import PrettyPrinter +from typing import List, Set, Union, Generic, TypeVar +from datastructures.stacks import Stack +from datastructures.graphs.vertex import Vertex +from datastructures.graphs.edge import Edge + +T = TypeVar("T") + + +class Graph(ABC, Generic[T]): + """ + Represents a Graph Data structure + """ + + def __init__(self, edge_list: List[Edge] = None): + if edge_list is None: + edge_list = [] + self.edge_list = edge_list + self.adjacency_list = defaultdict(List[Vertex]) + self.__construct_adjacency_list() + self.nodes = [] + self.node_count = len(self.nodes) + + def add(self, source_node: Vertex, destination_node: Vertex): + """ + Adds a connection between node_one and node_two + """ + source_node.neighbours.append(destination_node) + destination_node.neighbours.append(source_node) + edge = Edge(node_one=source_node, node_two=destination_node) + self.edge_list.append(edge) + self.adjacency_list[source_node].append(destination_node) + self.adjacency_list[destination_node].append(source_node) + self.nodes.append(source_node) + self.nodes.append(destination_node) + + def __construct_adjacency_list(self): + """ + Construct adjacency list + """ + for edge in self.edge_list: + self.adjacency_list[edge.node_one].append(edge.node_two) + + @abstractmethod + def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]: + """ + Given the root node to traverse and a target node, returns the BFS result of this Graph from the root node to + the target node + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def bfs_from_node(self, source: Vertex) -> Set[Vertex]: + """ + Given the source to traverse, returns the BFS result of this Graph from the source node + """ + raise NotImplementedError("Not yet implemented") + + def topological_sorted_order(self) -> List[Vertex]: + """ + Returns the topological sorted order of the Graph + """ + # These static variables are used to perform DFS recursion + # white nodes depict nodes that have not been visited yet + # gray nodes depict ongoing recursion + # black nodes depict recursion is complete + # An edge leading to a BLACK node is not a "cycle" + white = 1 + gray = 2 + black = 3 + + # Nothing to do here + if self.node_count == 0: + return [] + + is_possible = True + stack = Stack() + + # By default all nodes are WHITE + visited_nodes = {node: white for node in range(self.node_count)} + + def dfs(node: Vertex): + nonlocal is_possible + + # Don't recurse further if we found a cycle already + if not is_possible: + return + + # start recursion + visited_nodes[node] = gray + + # Traverse on neighbouring nodes/vertices + if node in self.adjacency_list: + for neighbour in self.adjacency_list[node]: + if visited_nodes[neighbour] == white: + dfs(node) + elif visited_nodes[node] == gray: + # An Edge to a Gray vertex/node represents a cycle + is_possible = False + + # Recursion ends. We mark if as BLACK + visited_nodes[node] = black + stack.push(node) + + for node in self.nodes: + # if the node is unprocessed, then call DFS on it + if visited_nodes[node] == white: + dfs(node) + + return list(stack.stack) if is_possible else [] + + def print(self): + pretty_print = PrettyPrinter() + pretty_print.pprint(self.adjacency_list) + + def remove(self, node: Vertex) -> None: + """ + Removes all references to a node + :param node + """ + for _, cxns in self.adjacency_list.items(): + try: + cxns.remove(node) + except KeyError: + pass + + try: + del self.adjacency_list[node] + except KeyError: + pass + + def is_connected(self, node_one: Vertex, node_two: Vertex) -> bool: + return ( + node_one in self.adjacency_list + and node_two in self.adjacency_list[node_two] + ) + + def find_path( + self, node_one: Vertex, node_two: Vertex, path=None + ) -> Union[List, None]: + """ + Find any path between node_one and node_two. May not be the shortest path + :param node_one + :param node_two + :param path + """ + + if path is None: + path = [] + path = [path] + [node_one] + + if node_one.data == node_two.data: + return path + + if node_one.data not in self.adjacency_list: + return None + + for node in self.adjacency_list[node_one]: + if node.data not in path: + new_path = self.find_path(node, node_two, path) + + if new_path: + return new_path + + return None + + def find_all_paths( + self, node_one: Vertex, node_two: Vertex, path: List = None + ) -> list: + """ + Finds all paths between node_one and node_two, where node_one is the start & node_two is the end + :param node_one Graph Node + :param node_two Graph Node + :param path + """ + if path is None: + path = [] + path = path + [node_one] + + if node_one.data == node_two.data: + return [path] + + if node_one.data not in self.adjacency_list: + return [] + + paths = [] + + for node in self.adjacency_list[node_one.data]: + if node not in path: + newpaths = self.find_all_paths(Vertex(node), node_two, path) + for newpath in newpaths: + paths.append(newpath) + + return paths + + def find_shortest_path( + self, node_one: Vertex, node_two: Vertex, path: List = None + ) -> Union[List, None]: + """ + Finds the shortest path between 2 nodes in the graph + """ + if path is None: + path = [] + + path = path + [node_one] + + if node_one.data == node_two.data: + return path + + if node_one.data not in self.adjacency_list: + return None + + shortest = None + + for node in self.adjacency_list[node_one]: + if node.data not in path: + newpath = self.find_shortest_path(node, node_two, path) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + + return shortest + + def __str__(self): + """ + Return string representation of this Graph + """ + return f"Graph: {self.adjacency_list}" diff --git a/datastructures/graphs/test_vertex.py b/datastructures/graphs/test_vertex.py new file mode 100644 index 00000000..cc313f42 --- /dev/null +++ b/datastructures/graphs/test_vertex.py @@ -0,0 +1,16 @@ +import unittest + +from datastructures.graphs import Vertex, UndirectedEdge + + +class VertexTestCases(unittest.TestCase): + def test_1(self): + node_one = Vertex(data=1) + node_two = Vertex(data=2) + edge = UndirectedEdge(node_one=node_one, node_two=node_two) + + self.assertEqual(True, False) # add assertion here + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/graphs/undirected/README.md b/datastructures/graphs/undirected/README.md new file mode 100644 index 00000000..3cd69233 --- /dev/null +++ b/datastructures/graphs/undirected/README.md @@ -0,0 +1,31 @@ +# Undirected Graph + +## Closest Node to Path in Tree + +You are given a positive integer, n, representing the number of nodes in a tree, numbered from `0 to n−1`. You are also +given a 2D integer array edges of length `n−1`, where `edges[i] = [ui, vi]` indicates that there is a bidirectional edge +connecting nodes `ui` and `vi`. + +You are also given a 2D integer array query of length `m`, where `query[i]=[starti ,endi ,nodei]`. For each query i, +find the node on the path between `starti` and `endi` that is closest to `nodei` in terms of the number of edges. + +Return an integer array where the value at index `i` corresponds to the answer for the `ith` query. + +> Note: If there are multiple such nodes at the same minimum distance, return the one with the smallest index. + +Constraints +- `1 ≤ n ≤ 1000` +- `edges.length == n-1` +- `edges[i].length == 2` +- `0 ≤ ui, vi ≤ n-1` +- `ui != vi` +- `1 ≤ query.length ≤ 1000` +- `query[i].length == 3` +- `0 ≤ starti, endi, nodei ≤ n-1` +- `the graph is a tree` + +### Examples + +![Example 1](./images/examples/closest_node_to_path_in_tree_example_1.png) +![Example 2](./images/examples/closest_node_to_path_in_tree_example_2.png) +![Example 3](./images/examples/closest_node_to_path_in_tree_example_3.png) diff --git a/datastructures/graphs/undirected/__init__.py b/datastructures/graphs/undirected/__init__.py index 8ecb943e..a44f7521 100644 --- a/datastructures/graphs/undirected/__init__.py +++ b/datastructures/graphs/undirected/__init__.py @@ -1,8 +1,4 @@ -from typing import List +from datastructures.graphs.undirected.undirected_graph import UndirectedGraph -from datastructures.graphs import Edge, Graph - -class UnDirectedGraph(Graph): - def __init__(self, edge_list: List[Edge]): - super(UnDirectedGraph, self).__init__(edge_list) +__all__ = ["UndirectedGraph"] diff --git a/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_1.png b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_1.png new file mode 100644 index 00000000..9962a6f3 Binary files /dev/null and b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_1.png differ diff --git a/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_2.png b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_2.png new file mode 100644 index 00000000..cc160d72 Binary files /dev/null and b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_2.png differ diff --git a/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_3.png b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_3.png new file mode 100644 index 00000000..7d2ce9b3 Binary files /dev/null and b/datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_3.png differ diff --git a/datastructures/graphs/undirected/test_utils.py b/datastructures/graphs/undirected/test_utils.py new file mode 100644 index 00000000..6b04b1bc --- /dev/null +++ b/datastructures/graphs/undirected/test_utils.py @@ -0,0 +1,37 @@ +import unittest +from typing import List +from parameterized import parameterized +from datastructures.graphs.undirected.utils import closest_node + + +class ClosestNodeToPathInTreeTestCase(unittest.TestCase): + + @parameterized.expand( + [ + (3, [[0, 1], [1, 2]], [[0, 2, 1]], [1]), + (4, [[0, 1], [1, 2], [1, 3]], [[2, 3, 0]], [1]), + ( + 6, + [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5]], + [[1, 5, 2], [2, 3, 4]], + [0, 0], + ), + ( + 7, + [[0, 1], [0, 2], [0, 3], [1, 4], [2, 5], [2, 6]], + [[5, 3, 4], [5, 3, 6]], + [0, 2], + ), + (3, [[0, 1], [1, 2]], [[0, 1, 2]], [1]), + (3, [[0, 1], [1, 2]], [[0, 0, 0]], [0]), + ] + ) + def test_closest_node( + self, n: int, edges: List[List[int]], query: List[List[int]], expected: int + ): + actual = closest_node(n, edges, query) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/graphs/undirected/undirected_graph.py b/datastructures/graphs/undirected/undirected_graph.py new file mode 100644 index 00000000..64009263 --- /dev/null +++ b/datastructures/graphs/undirected/undirected_graph.py @@ -0,0 +1,15 @@ +from typing import List, Set + +from datastructures.graphs import Edge, Graph, Vertex + + +class UndirectedGraph(Graph): + + def __init__(self, edge_list: List[Edge]): + super(UndirectedGraph, self).__init__(edge_list) + + def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]: + pass + + def bfs_from_node(self, source: Vertex) -> Set[Vertex]: + pass diff --git a/datastructures/graphs/undirected/utils.py b/datastructures/graphs/undirected/utils.py new file mode 100644 index 00000000..d1445643 --- /dev/null +++ b/datastructures/graphs/undirected/utils.py @@ -0,0 +1,94 @@ +from typing import List, DefaultDict, Dict, Optional +from collections import defaultdict, deque + + +def closest_node(n: int, edges: List[List[int]], query: List[List[int]]) -> List[int]: + # build and adjacency list + adj_list: DefaultDict[int, List[int]] = defaultdict(list) + for u, v in edges: + adj_list[u].append(v) + adj_list[v].append(u) + + def bfs_distance(start: int) -> List[int]: + """ + Compute the shortest distance from start node to all other nodes using BFS. + Returns a list where distances[i] is the distance from start to node i. + """ + # -1 means not visited + distances = [-1] * n + distances[start] = 0 + queue = deque([start]) + + while queue: + node = queue.popleft() + for neighbor in adj_list[node]: + if distances[neighbor] == -1: # Not visited + distances[neighbor] = distances[node] + 1 + queue.append(neighbor) + + return distances + + def find_path(start: int, end: int) -> List[int]: + """ + Find the unique path from start to end in the tree using BFS. + Returns the list of nodes on the path (including start and end). + """ + if start == end: + return [start] + + # BFS to find path, keeping track of parent pointers + parent: Dict[int, Optional[int]] = {start: None} + queue = deque([start]) + + while queue: + node = queue.popleft() + if node == end: + break + + for neighbor in adj_list[node]: + # not visited + if neighbor not in parent: + parent[neighbor] = node + queue.append(neighbor) + + # Reconstruct path from end to start using pointers + path = [] + current = end + # Add safety check: limit iterations to avoid infinite loops + # In a tree with n nodes, path length can't exceed n + for _ in range(n): + if current not in parent: + # This shouldn't happen in a valid connected tree + break + path.append(current) + if current == start: + # We've reached the start, we're done + break + current = parent[current] + + # reverse to get the path start to end + return path[::-1] + + result = [] + for start, end, target in query: + # find the path from start to end + found_path = find_path(start, end) + + # compute distances from target node to all nodes + distances_from_target = bfs_distance(target) + + # Find the node on the path with minimum distance to target + # If there's a tie, we want the one with the smallest index + min_distance = float("inf") + closest = -1 + + for node in found_path: + dist = distances_from_target[node] + # Update if we found a closer node, or same distance but smaller index + if dist < min_distance or (dist == min_distance and node < closest): + min_distance = dist + closest = node + + result.append(closest) + + return result diff --git a/datastructures/graphs/vertex.py b/datastructures/graphs/vertex.py index 1c144371..932a39c2 100644 --- a/datastructures/graphs/vertex.py +++ b/datastructures/graphs/vertex.py @@ -13,16 +13,21 @@ class Vertex(Generic[T]): def __init__( self, data: T, - incoming_edges: Set[Edge], - outgoing_edges: Set[Edge], - properties: Optional[Dict[str, Any]] = None, identifier: Any = uuid4(), + properties: Optional[Dict[str, Any]] = None, + incoming_edges: Optional[Set[Edge]] = None, + outgoing_edges: Optional[Set[Edge]] = None, ): + if outgoing_edges is None: + outgoing_edges = set() + if incoming_edges is None: + incoming_edges = set() self.id = identifier self.data = data self.incoming_edges = incoming_edges self.outgoing_edges = outgoing_edges self.edges = self.incoming_edges.union(self.outgoing_edges) + self.adjacent_vertices: Dict[str, "Vertex"] = {} self.properties = properties def __str__(self): @@ -31,24 +36,35 @@ def __str__(self): f"Degree: {self.degree}" ) + def __eq__(self, other: "Vertex") -> bool: + return self.id == other.id + + def add_adjacent_vertex(self, other: "Vertex") -> None: + """Adds an adjacent vertex to the list of neighbors. Note that this is useful in a graph as the graph will be + able the call this method on this vertex and the same method on the other vertex showing undirected relationship. + + Args: + other (Vertex): Vertex to add as a neighbor + """ + # should not be able to add self as an adjacent vertex + if other is self or other.id == self.id: + return + + # only add adjacent vertex if not already present. + if not self.adjacent_vertices.get(other.id): + self.adjacent_vertices[other.id] = other + other.add_adjacent_vertex(self) + @property def neighbours(self) -> List["Vertex"]: - """Returns a list of all the direct neighbours of this vertex + """Returns a list of all the direct neighbors of this vertex Returns: - List: list of vertices that are direct neighbours or this vertex + List: list of vertices that are direct neighbors or this vertex """ nodes = [] - for edge in self.incoming_edges: - node = edge.source - if node.id != self.id: - nodes.append(node) - nodes.append(node) - - for edge in self.outgoing_edges: - node = edge.destination - if node.id != self.id: - nodes.append(node) + for vertex in self.adjacent_vertices.values(): + nodes.append(vertex) return nodes @@ -61,10 +77,10 @@ def degree(self) -> int: """ degrees = 0 - if len(self.incoming_edge) == 0 or len(self.outgoing_edges) == 0: + if len(self.incoming_edges) == 0 or len(self.outgoing_edges) == 0: return degrees - seen_edges: Set = {} + seen_edges: Set = set() for edge in self.edges: if edge not in seen_edges: @@ -87,7 +103,7 @@ def in_degree(self) -> int: return in_degrees for edge in self.edges: - if edge.type == EdgeType.DIRECTED and edge.destination == self: + if edge.type == EdgeType.DIRECTED and edge.node_two == self: in_degrees += 1 return in_degrees @@ -105,18 +121,7 @@ def out_degree(self) -> int: return out_degrees for edge in self.edges: - if edge.type == EdgeType.DIRECTED and edge.source == self: + if edge.type == EdgeType.DIRECTED and edge.node_one == self: out_degrees += 1 return out_degrees - - def add_adjacent_vertex(self, other: "Vertex"): - """Adds an adjacent vertex to the list of neighbors. Note that this is useful in a graph as the graph will be - able the call this method on this vertex & the same method on the other vertex showing undirected relationship. - - Args: - other (Vertex): Vertex to add as a neighbor - """ - - def __eq__(self, other: "Vertex") -> bool: - return self.id == other.id