diff --git a/graphs/edmond_blossom_algorithm.py b/graphs/edmond_blossom_algorithm.py new file mode 100644 index 000000000000..57dce7f9efcc --- /dev/null +++ b/graphs/edmond_blossom_algorithm.py @@ -0,0 +1,223 @@ +from collections import deque, defaultdict + +UNMATCHED = -1 # Constant to represent unmatched vertices + + +class EdmondsBlossomAlgorithm: + @staticmethod + def maximum_matching(edges, vertex_count): + """ + Finds the maximum matching in a general graph using Edmonds' Blossom Algorithm. + + :param edges: List of edges in the graph (each edge is a tuple of two vertices). + :param vertex_count: The number of vertices in the graph. + :return: A list of matched pairs of vertices (tuples). + """ + # Initialize the adjacency list for the graph + graph = defaultdict(list) + + # Populate the graph with the edges + for u, v in edges: + graph[u].append(v) + graph[v].append(u) + + # Initialize matching array and other auxiliary structures + match = [ + UNMATCHED + ] * vertex_count # Stores matches for each vertex, initially unmatched + parent = [ + UNMATCHED + ] * vertex_count # Tracks the parent of each vertex in the augmenting path + base = list( + range(vertex_count) + ) # Base of each vertex (initially the vertex itself) + in_blossom = [False] * vertex_count # Marks if a vertex is part of a blossom + in_queue = [ + False + ] * vertex_count # Marks if a vertex is already in the BFS queue + + # Iterate over each vertex to start the matching process + for u in range(vertex_count): + if match[u] == UNMATCHED: # Proceed only if the vertex is unmatched + # Reset auxiliary data structures for BFS + parent = [UNMATCHED] * vertex_count + base = list( + range(vertex_count) + ) # Each vertex is its own base initially + in_blossom = [False] * vertex_count + in_queue = [False] * vertex_count + + queue = deque([u]) # Initialize BFS queue + in_queue[u] = True # Mark u as in the queue + + augmenting_path_found = ( + False # Flag to track if an augmenting path is found + ) + + # BFS to find augmenting paths + while queue and not augmenting_path_found: + current = queue.popleft() # Get the next vertex from the queue + + for y in graph[current]: + # Skip if this edge is the current matching + if match[current] == y: + continue + + # Avoid self-loops + if base[current] == base[y]: + continue + + # Case 1: y is unmatched, we found an augmenting path + if parent[y] == UNMATCHED: + if match[y] == UNMATCHED: + parent[y] = current + augmenting_path_found = True + EdmondsBlossomAlgorithm.update_matching( + match, parent, y + ) # Augment the path + break + + # Case 2: y is matched, add y's match to the queue + z = match[y] + parent[y] = current + parent[z] = y + if not in_queue[z]: + queue.append(z) + in_queue[z] = True + else: + # Case 3: A cycle (blossom) is detected + base_u = EdmondsBlossomAlgorithm.find_base( + base, parent, current, y + ) + if base_u != UNMATCHED: + aux_data = BlossomAuxData( + queue, parent, base, in_blossom, match, in_queue + ) + EdmondsBlossomAlgorithm.contract_blossom( + BlossomData(aux_data, current, y, base_u) + ) + + # Collect the final matching result + matching_result = [] + for v in range(vertex_count): + if match[v] != UNMATCHED and v < match[v]: + matching_result.append((v, match[v])) + + return matching_result + + @staticmethod + def update_matching(match, parent, u): + """ + Updates the matching array by augmenting along the found path. + + :param match: The array representing the matching for each vertex. + :param parent: The parent array, used to backtrack the augmenting path. + :param u: The end vertex of the augmenting path. + """ + while u != UNMATCHED: + v = parent[u] + next_node = match[v] + match[v] = u + match[u] = v + u = next_node + + @staticmethod + def find_base(base, parent, u, v): + """ + Finds the lowest common ancestor (LCA) of two vertices in the blossom. + + :param base: The array storing the base of each vertex in the current blossom. + :param parent: The parent array used to trace the path. + :param u: One end of the edge being analyzed. + :param v: The other end of the edge being analyzed. + :return: The base of the node (LCA) or UNMATCHED if not found. + """ + visited = [False] * len(base) + + # Mark all ancestors of u + current_u = u + while True: + current_u = base[current_u] + visited[current_u] = True + if parent[current_u] == UNMATCHED: + break + current_u = parent[current_u] + + # Find the common ancestor with v + current_v = v + while True: + current_v = base[current_v] + if visited[current_v]: + return current_v + current_v = parent[current_v] + + @staticmethod + def contract_blossom(blossom_data): + """ + Contracts a blossom in the graph, modifying the base array and marking the vertices involved. + + :param blossom_data: An object containing the necessary data to perform the contraction. + """ + # Mark all vertices in the blossom + for x in range( + blossom_data.u, + blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca, + blossom_data.aux_data.parent[blossom_data.aux_data.match[x]], + ): + base_x = blossom_data.aux_data.base[x] + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + blossom_data.aux_data.in_blossom[base_x] = True + blossom_data.aux_data.in_blossom[match_base_x] = True + + for x in range( + blossom_data.v, + blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca, + blossom_data.aux_data.parent[blossom_data.aux_data.match[x]], + ): + base_x = blossom_data.aux_data.base[x] + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + blossom_data.aux_data.in_blossom[base_x] = True + blossom_data.aux_data.in_blossom[match_base_x] = True + + # Update the base for all marked vertices + for i in range(len(blossom_data.aux_data.base)): + if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]: + blossom_data.aux_data.base[i] = blossom_data.lca + if not blossom_data.aux_data.in_queue[i]: + blossom_data.aux_data.queue.append(i) + blossom_data.aux_data.in_queue[i] = True + + +class BlossomAuxData: + """ + A helper class to encapsulate auxiliary data used during blossom contraction. + """ + + def __init__(self, queue, parent, base, in_blossom, match, in_queue): + self.queue = queue # The BFS queue used for traversal + self.parent = parent # The parent array for each vertex + self.base = base # The base array indicating the base of each vertex + self.in_blossom = in_blossom # Indicates whether a vertex is part of a blossom + self.match = match # The matching array + self.in_queue = in_queue # Indicates whether a vertex is already in the queue + + +class BlossomData: + """ + A helper class to store parameters necessary for blossom contraction. + """ + + def __init__(self, aux_data, u, v, lca): + self.aux_data = aux_data # The auxiliary data object + self.u = u # One vertex in the current edge + self.v = v # The other vertex in the current edge + self.lca = lca # The lowest common ancestor (base) of the current blossom + + +# Example usage: +if __name__ == "__main__": + # Example graph with 6 vertices + edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (2, 4)] + vertex_count = 6 + result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) + print("Maximum Matching Pairs:", result) diff --git a/graphs/edmonds_karp_multiple_source_and_sink.py b/graphs/edmonds_karp_multiple_source_and_sink.py index 5c774f4b812b..7d5b6f5b2215 100644 --- a/graphs/edmonds_karp_multiple_source_and_sink.py +++ b/graphs/edmonds_karp_multiple_source_and_sink.py @@ -1,193 +1,247 @@ -class FlowNetwork: - def __init__(self, graph, sources, sinks): - self.source_index = None - self.sink_index = None - self.graph = graph - - self._normalize_graph(sources, sinks) - self.vertices_count = len(graph) - self.maximum_flow_algorithm = None - - # make only one source and one sink - def _normalize_graph(self, sources, sinks): - if sources is int: - sources = [sources] - if sinks is int: - sinks = [sinks] - - if len(sources) == 0 or len(sinks) == 0: - return - - self.source_index = sources[0] - self.sink_index = sinks[0] - - # make fake vertex if there are more - # than one source or sink - if len(sources) > 1 or len(sinks) > 1: - max_input_flow = 0 - for i in sources: - max_input_flow += sum(self.graph[i]) - - size = len(self.graph) + 1 - for room in self.graph: - room.insert(0, 0) - self.graph.insert(0, [0] * size) - for i in sources: - self.graph[0][i + 1] = max_input_flow - self.source_index = 0 - - size = len(self.graph) + 1 - for room in self.graph: - room.append(0) - self.graph.append([0] * size) - for i in sinks: - self.graph[i + 1][size - 1] = max_input_flow - self.sink_index = size - 1 - - def find_maximum_flow(self): - if self.maximum_flow_algorithm is None: - raise Exception("You need to set maximum flow algorithm before.") - if self.source_index is None or self.sink_index is None: - return 0 - - self.maximum_flow_algorithm.execute() - return self.maximum_flow_algorithm.getMaximumFlow() - - def set_maximum_flow_algorithm(self, algorithm): - self.maximum_flow_algorithm = algorithm(self) - - -class FlowNetworkAlgorithmExecutor: - def __init__(self, flow_network): - self.flow_network = flow_network - self.verticies_count = flow_network.verticesCount - self.source_index = flow_network.sourceIndex - self.sink_index = flow_network.sinkIndex - # it's just a reference, so you shouldn't change - # it in your algorithms, use deep copy before doing that - self.graph = flow_network.graph - self.executed = False - - def execute(self): - if not self.executed: - self._algorithm() - self.executed = True - - # You should override it - def _algorithm(self): - pass - - -class MaximumFlowAlgorithmExecutor(FlowNetworkAlgorithmExecutor): - def __init__(self, flow_network): - super().__init__(flow_network) - # use this to save your result - self.maximum_flow = -1 - - def get_maximum_flow(self): - if not self.executed: - raise Exception("You should execute algorithm before using its result!") - - return self.maximum_flow - - -class PushRelabelExecutor(MaximumFlowAlgorithmExecutor): - def __init__(self, flow_network): - super().__init__(flow_network) - - self.preflow = [[0] * self.verticies_count for i in range(self.verticies_count)] - - self.heights = [0] * self.verticies_count - self.excesses = [0] * self.verticies_count - - def _algorithm(self): - self.heights[self.source_index] = self.verticies_count - - # push some substance to graph - for nextvertex_index, bandwidth in enumerate(self.graph[self.source_index]): - self.preflow[self.source_index][nextvertex_index] += bandwidth - self.preflow[nextvertex_index][self.source_index] -= bandwidth - self.excesses[nextvertex_index] += bandwidth - - # Relabel-to-front selection rule - vertices_list = [ - i - for i in range(self.verticies_count) - if i not in {self.source_index, self.sink_index} - ] - - # move through list - i = 0 - while i < len(vertices_list): - vertex_index = vertices_list[i] - previous_height = self.heights[vertex_index] - self.process_vertex(vertex_index) - if self.heights[vertex_index] > previous_height: - # if it was relabeled, swap elements - # and start from 0 index - vertices_list.insert(0, vertices_list.pop(i)) - i = 0 - else: - i += 1 - - self.maximum_flow = sum(self.preflow[self.source_index]) - - def process_vertex(self, vertex_index): - while self.excesses[vertex_index] > 0: - for neighbour_index in range(self.verticies_count): - # if it's neighbour and current vertex is higher - if ( - self.graph[vertex_index][neighbour_index] - - self.preflow[vertex_index][neighbour_index] - > 0 - and self.heights[vertex_index] > self.heights[neighbour_index] - ): - self.push(vertex_index, neighbour_index) - - self.relabel(vertex_index) - - def push(self, from_index, to_index): - preflow_delta = min( - self.excesses[from_index], - self.graph[from_index][to_index] - self.preflow[from_index][to_index], - ) - self.preflow[from_index][to_index] += preflow_delta - self.preflow[to_index][from_index] -= preflow_delta - self.excesses[from_index] -= preflow_delta - self.excesses[to_index] += preflow_delta - - def relabel(self, vertex_index): - min_height = None - for to_index in range(self.verticies_count): - if ( - self.graph[vertex_index][to_index] - - self.preflow[vertex_index][to_index] - > 0 - ) and (min_height is None or self.heights[to_index] < min_height): - min_height = self.heights[to_index] - - if min_height is not None: - self.heights[vertex_index] = min_height + 1 - - -if __name__ == "__main__": - entrances = [0] - exits = [3] - # graph = [ - # [0, 0, 4, 6, 0, 0], - # [0, 0, 5, 2, 0, 0], - # [0, 0, 0, 0, 4, 4], - # [0, 0, 0, 0, 6, 6], - # [0, 0, 0, 0, 0, 0], - # [0, 0, 0, 0, 0, 0], - # ] - graph = [[0, 7, 0, 0], [0, 0, 6, 0], [0, 0, 0, 8], [9, 0, 0, 0]] - - # prepare our network - flow_network = FlowNetwork(graph, entrances, exits) - # set algorithm - flow_network.set_maximum_flow_algorithm(PushRelabelExecutor) - # and calculate - maximum_flow = flow_network.find_maximum_flow() - - print(f"maximum flow is {maximum_flow}") +from collections import deque, defaultdict +from typing import List, Tuple, Dict + + +UNMATCHED = -1 # Constant to represent unmatched vertices + + +class EdmondsBlossomAlgorithm: + @staticmethod + def maximum_matching( + edges: List[Tuple[int, int]], vertex_count: int + ) -> List[Tuple[int, int]]: + """ + Finds the maximum matching in a general graph using Edmonds' Blossom Algorithm. + + :param edges: List of edges in the graph. + :param vertex_count: Number of vertices in the graph. + :return: A list of matched pairs of vertices. + + >>> EdmondsBlossomAlgorithm.maximum_matching([(0, 1), (1, 2), (2, 3)], 4) + [(0, 1), (2, 3)] + """ + graph: Dict[int, List[int]] = defaultdict(list) + + # Populate the graph with the edges + for vertex_u, vertex_v in edges: + graph[vertex_u].append(vertex_v) + graph[vertex_v].append(vertex_u) + + # Initial matching array and auxiliary data structures + match = [UNMATCHED] * vertex_count + parent = [UNMATCHED] * vertex_count + base = list(range(vertex_count)) + in_blossom = [False] * vertex_count + in_queue = [False] * vertex_count + + # Main logic for finding maximum matching + for vertex_u in range(vertex_count): + if match[vertex_u] == UNMATCHED: + # BFS initialization + parent = [UNMATCHED] * vertex_count + base = list(range(vertex_count)) + in_blossom = [False] * vertex_count + in_queue = [False] * vertex_count + + queue = deque([vertex_u]) + in_queue[vertex_u] = True + + augmenting_path_found = False + + # BFS to find augmenting paths + while queue and not augmenting_path_found: + current_vertex = queue.popleft() + for neighbor in graph[current_vertex]: + if match[current_vertex] == neighbor: + continue + + if base[current_vertex] == base[neighbor]: + continue # Avoid self-loops + + if parent[neighbor] == UNMATCHED: + # Case 1: neighbor is unmatched, we've found an augmenting path + if match[neighbor] == UNMATCHED: + parent[neighbor] = current_vertex + augmenting_path_found = True + EdmondsBlossomAlgorithm.update_matching( + match, parent, neighbor + ) + break + + # Case 2: neighbor is matched, add neighbor's match to the queue + matched_vertex = match[neighbor] + parent[neighbor] = current_vertex + parent[matched_vertex] = neighbor + if not in_queue[matched_vertex]: + queue.append(matched_vertex) + in_queue[matched_vertex] = True + else: + # Case 3: Both current_vertex and neighbor have a parent; check for a cycle/blossom + base_vertex = EdmondsBlossomAlgorithm.find_base( + base, parent, current_vertex, neighbor + ) + if base_vertex != UNMATCHED: + EdmondsBlossomAlgorithm.contract_blossom( + BlossomData( + BlossomAuxData( + queue, + parent, + base, + in_blossom, + match, + in_queue, + ), + current_vertex, + neighbor, + base_vertex, + ) + ) + + # Create result list of matched pairs + matching_result = [] + for vertex in range(vertex_count): + if match[vertex] != UNMATCHED and vertex < match[vertex]: + matching_result.append((vertex, match[vertex])) + + return matching_result + + @staticmethod + def update_matching( + match: List[int], parent: List[int], current_vertex: int + ) -> None: + """ + Updates the matching along the augmenting path found. + + :param match: The matching array. + :param parent: The parent array used during the BFS. + :param current_vertex: The starting node of the augmenting path. + + >>> match = [UNMATCHED, UNMATCHED, UNMATCHED] + >>> parent = [1, 0, UNMATCHED] + >>> EdmondsBlossomAlgorithm.update_matching(match, parent, 2) + >>> match + [1, 0, -1] + """ + while current_vertex != UNMATCHED: + matched_vertex = parent[current_vertex] + next_vertex = match[matched_vertex] + match[matched_vertex] = current_vertex + match[current_vertex] = matched_vertex + current_vertex = next_vertex + + @staticmethod + def find_base( + base: List[int], parent: List[int], vertex_u: int, vertex_v: int + ) -> int: + """ + Finds the base of a node in the blossom. + + :param base: The base array. + :param parent: The parent array. + :param vertex_u: One end of the edge. + :param vertex_v: The other end of the edge. + :return: The base of the node or UNMATCHED. + + >>> base = [0, 1, 2, 3] + >>> parent = [1, 0, UNMATCHED, UNMATCHED] + >>> EdmondsBlossomAlgorithm.find_base(base, parent, 2, 3) + 2 + """ + visited = [False] * len(base) + + # Mark ancestors of vertex_u + current_vertex_u = vertex_u + while True: + current_vertex_u = base[current_vertex_u] + visited[current_vertex_u] = True + if parent[current_vertex_u] == UNMATCHED: + break + current_vertex_u = parent[current_vertex_u] + + # Find the common ancestor of vertex_v + current_vertex_v = vertex_v + while True: + current_vertex_v = base[current_vertex_v] + if visited[current_vertex_v]: + return current_vertex_v + current_vertex_v = parent[current_vertex_v] + + @staticmethod + def contract_blossom(blossom_data: "BlossomData") -> None: + """ + Contracts a blossom in the graph, modifying the base array + and marking the vertices involved. + + :param blossom_data: An object containing the necessary data + to perform the contraction. + + >>> aux_data = BlossomAuxData(deque(), [], [], [], [], []) + >>> blossom_data = BlossomData(aux_data, 0, 1, 2) + >>> EdmondsBlossomAlgorithm.contract_blossom(blossom_data) + """ + # Mark all vertices in the blossom + current_vertex_u = blossom_data.u + while blossom_data.aux_data.base[current_vertex_u] != blossom_data.lca: + base_u = blossom_data.aux_data.base[current_vertex_u] + match_base_u = blossom_data.aux_data.base[ + blossom_data.aux_data.match[current_vertex_u] + ] + blossom_data.aux_data.in_blossom[base_u] = True + blossom_data.aux_data.in_blossom[match_base_u] = True + current_vertex_u = blossom_data.aux_data.parent[ + blossom_data.aux_data.match[current_vertex_u] + ] + + current_vertex_v = blossom_data.v + while blossom_data.aux_data.base[current_vertex_v] != blossom_data.lca: + base_v = blossom_data.aux_data.base[current_vertex_v] + match_base_v = blossom_data.aux_data.base[ + blossom_data.aux_data.match[current_vertex_v] + ] + blossom_data.aux_data.in_blossom[base_v] = True + blossom_data.aux_data.in_blossom[match_base_v] = True + current_vertex_v = blossom_data.aux_data.parent[ + blossom_data.aux_data.match[current_vertex_v] + ] + + # Update the base for all marked vertices + for i in range(len(blossom_data.aux_data.base)): + if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]: + blossom_data.aux_data.base[i] = blossom_data.lca + if not blossom_data.aux_data.in_queue[i]: + blossom_data.aux_data.queue.append(i) + blossom_data.aux_data.in_queue[i] = True + + +class BlossomAuxData: + """ + Auxiliary data class to encapsulate common parameters for the blossom operations. + """ + + def __init__( + self, + queue: deque, + parent: List[int], + base: List[int], + in_blossom: List[bool], + match: List[int], + in_queue: List[bool], + ) -> None: + self.queue = queue + self.parent = parent + self.base = base + self.in_blossom = in_blossom + self.match = match + self.in_queue = in_queue + + +class BlossomData: + """ + BlossomData class with reduced parameters. + """ + + def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int) -> None: + self.aux_data = aux_data + self.u = u + self.v = v + self.lca = lca