# Bipartite Matching ```python import numpy as np class Graph (object): ''' A flow graph object Calculates max flow using different methods ''' def __init__(self, data=None): n, m = np.shape(data) assert n == m, "data must be square" # edge properties self.capacity = data self.flow = np.zeros((n, n), dtype=np.int64) self.size = n # vertices properties self.vertices = np.array([i for i in range(self.size)]) self.excess = [0] * self.size self.distance = [0] * self.size self.level = [-1] * self.size self.seen = [0] * self.size assert sum(self.capacity[-1]) == 0 and sum(self.capacity[:, 0]) \ == 0 , "source must be first, sink must be last" self.source = 0 self.sink = self.size - 1 def _search(self, traverse, origin=None, goal=None, DFS=False): ''' objective : find an augmented path between given origin and goal. uses DFS or BFS parameters : self - object traverse - a 1D array, holds the traverse of the path, traverse[successor] = current origin - int, an index for the origin, the default is the source goal - int, an index for the goal, the default is sink DFS - bool, True will search depth-first and False will search breadth-first. the default is BFS output True or False; indicator whether a path exists ''' # set path origin and goal indexes goal = goal or self.sink origin = origin or self.source # avoid double visit visited = [False] * self.size # a queue that states the next indexes to visit, starting with origin to_visit = [origin] # mark origin as visited visited[origin] = True # to determine the edge; predecessor:u predecessor = origin while to_visit: # pops the next index to visit, base of the search method chosen if DFS: current = to_visit.pop(0) # BFS else: current = to_visit.pop() # iterates on all possible steps for successor in range(self.size): # a viable next step is one that was not visited and that its # residual is possitive if (not visited[successor]) and \ (self.capacity[(current, successor)] \ - self.flow[(current, successor)] > 0): # mark as visited and add to the queue to explore visited[successor] = True to_visit.append(successor) # adds to the traverse traverse[successor] = current # print(visited) # print(successor) return visited[goal] def _max_flow_search_FF(self, origin=None, goal=None, data=False,\ DFS=False): ''' objective : finds the max flow for a given graph parameters : self - object origin - int, an index for the origin, the default is the source goal - int, an index for the goal, the default is sink data - bool, returns extended information if marked True DFS - bool, True will search depth-first and False will search breadth-first. the default is BFS output default - float of max flow with data - {max_flow: float of max flow, iteration: a dictionary with all traverses taken} complexity O(VE^2) ''' goal = goal or self.sink origin = origin or self.source #extendend information presented_data = {} # initiate vars traverse = [-1] * self.size max_flow = 0 iter_count = 0 path_flow = float('inf') # before augmented path were exhusted while self._search(traverse, origin, goal, DFS): iter_count += 1 presented_data[iter_count] = [] # walk backwards, find the smallest viable flow # for an augmented path current = goal while current != origin : presented_data[iter_count].append(current) # the predecessor for the current index pred = traverse[current] # calculate the residual, the path flow is the smaller # between the edge residual and the existinf flow residual_flow = (self.capacity[(pred, current)] - \ self.flow[(pred, current)]) path_flow = min(path_flow, residual_flow) current = pred # add the current path flow to the total flow max_flow += path_flow # walk backwards, update the flow on the edges based on # the path flow current = goal while current != origin : # the predecessor for the current index pred = traverse[current] self.flow[(pred, current)] += path_flow self.flow[(current, pred)] -= path_flow current = pred if data: return {'max_flow' : max_flow, 'iteration': \ [{i: [0] + presented_data[i][::-1]} \ for i in range(1, iter_count+1)]} return max_flow def EdmondKarp(self, origin=None, goal=None, data=False): ''' objective : an external func, uses _max_flow_search with BFS Finds the shortest augmenting path from s to t, using BFS. For each path, it finds the bounding capacity and sends as much flow. This process is repeating until there are no more augmented path in the residual graph parameters : self - object origin - int, an index for the origin, the default is the source goal - int, an index for the goal, the default is sink data - bool, returns extended information if marked True output default - float of max flow with data - {max_flow: float of max flow, iteration: a dictionary with all traverses taken} complexity O(VE^2) This algorithm uses a BFS to fins in each iteration the shortest augmented path between the source and the sink. Each path contains at least one edge. A single BFS runs O(E) and a push along the path is in its worst case O(E). Since the length of th paths never decreases (as promised by the use of BFS) and the length of a path can never exceed V, each edge can be found V times. The total complexity, therefore, O(VE^2) ''' return self._max_flow_search_FF(origin=origin, goal=goal, \ data=data, DFS=True) def _BFS_using_levels(self, origin=None, goal=None): ''' objective : bfs using the vertex level parameters : origin: int, the default is the source goal: int, the default is the sink output: bool: True if the goals level is higher than 0 ''' # set path origin and goal indexes origin = origin or self.source goal = goal or self.sink self.level = [-1] * self.size self.level[origin] = 0 to_visit = [origin] while to_visit : current = to_visit.pop(0) for successor in range(self.size): if self.capacity[(current, successor)] - \ self.flow[(current, successor)] \ > 0 and self.level[successor] < 0: self.level[successor] = self.level[current] + 1 to_visit.append(successor) return self.level def _send_flow(self, origin=None, goal=None, max_flow=float('inf')): ''' objective : find the max flow based on DFS parameters : origin: int, the default is the source goal: int, the default is the sink max_flow: int, default is the inf output : 0 if no path, flow if path found ''' goal = goal or self.sink origin = origin or self.source # sink reached if origin == goal : return max_flow # traverse all neighbors for successor in range(self.size): # if a viable edge, a positive residual and d(u)<d(v) if ((self.capacity[(origin, successor)] - \ self.flow[(origin, successor)] > 0 ) and (self.level[origin] + 1 == self.level[successor])): # finds the bounding flow current_flow = \ min(max_flow, self.capacity[(origin, successor)] \ - self.flow[(origin, successor)]) path_flow = self._send_flow(successor, goal, current_flow) # if the flow is viable, update the path. if path_flow > 0: self.flow[(origin, successor)] += path_flow self.flow[(successor, origin)] -= path_flow return path_flow return 0 def _max_flow_search_D(self, origin=None, goal=None): ''' objective : iteration on the augmented paths parameters : origin: int, the default is the source goal: int, the default is the sink output: the graph's max flow ''' # initialzing vars goal = goal or self.sink origin = origin or self.source total_flow = 0 # while there is path on the residual graph while True : # finds the path using BFS and marks their reachability to the sink self.level = self._BFS_using_levels(origin, goal) # if reached sink if self.level[goal] < 0: return total_flow # using the above paths to compute flow while True: path_flow = self._send_flow(origin, goal) if path_flow == 0: break # incrementing flow total_flow += path_flow def Dinic(self, origin=None, goal=None): ''' objective: creates the next path using BFS, marks vertexes based on their distance from s. choosing only edges (u,v) such d(u)<d(v), resulting in a graph with fewer edges, and creates a graph where all path have the same length, found by a single DFS. Repeating only on the part of the graph that reaches t (using the found blocking flow) ** performance exceeds EK for dense graphs. parameters : origin: int, the default is the source goal: int, the default is the sink max_flow: int, default is the inf output: max flow complexity: O(EV^2) Each iteration finds a longer set of paths than the previous one. Each DFS running on these paths (removing the unreachable vertexes) takes O(E). The maximum length of these paths is V, going through all vertexes. Hence, finding a viable path takes O(VE). This process can be repeated bounded by the number of vertexes, hence O(V). Reaching total complexity of O(EV^2) ''' return self._max_flow_search_D(origin=origin, goal=goal) def _push(self, u, v): ''' objective: pushes the excess to a viable next vertex parameters : self - object v - int, index of an overflowing vertex u - int, index of a potential neighbor to move excess to output none ''' # caculates the viable transferable flow, residual or the full excess delta_flow = min(self.excess[u], (self.capacity[(u, v)] - \ self.flow[(u, v)])) # modify the flow and excess in both vertexes, and edges, # based on the delta in flow self.flow[(u, v)] += delta_flow self.flow[(v, u)] -= delta_flow self.excess[u] -= delta_flow self.excess[v] += delta_flow def _relabel(self, u): ''' objective : modifies a vertex distnce to enable push (to create d(u) < d(v)) parameters : self - object u - int, index of the vertex to change the distance output none ''' # initialize min_distance = float('inf') # finds the min distance possible and changes the given # vertex distance by this number for v in range(self.size): if (self.capacity[(u, v)] - self.flow[(u, v)]) > 0 : min_distance = min(min_distance, self.distance[v]) self.distance[u] = min_distance + 1 def _discharge(self, u): ''' objective: iterating on an active vertex with an excess until resolved parameters : self - object u - int, index of a vertex with an excess flow, keep pushing flow until it disactivates (= excess flow = 0) output none ''' while self.excess[u] > 0 : # iterating through all possibel vertexes if self.seen[u] < self.size : v = self.seen[u] # verifies the conditions to push: positive residual on the edge # and lower distance on the vertex the flow will be pushed to # pushes if viable or moves to the next vertex if (self.capacity[(u, v)] - self.flow[(u, v)] > 0) \ and (self.distance[u] > self.distance[v]): self._push(u, v) else: self.seen[u] += 1 # if a push is not possible, relabel else: self._relabel(u) self.seen[u] = 0 def PushRelable(self, origin=None, goal=None): ''' objective: unlike the previous two, this algorithm optimizes based on local decisions. Front to back, this algorithm iterates on the list of vertexes in the graph repeatedly selecting an overflowing vertex and discharge it: using push and relabel until the vertex deactivates. ** performance exceeds Dinic for dense graphs. parameters : self - object origin - int, an index for the origin, the default is the source goal - int, an index for the goal, the default is sink output graph max flow complexity O(V^3) Each vertex can be relabeled 2V times; discahrging pushes (uses the full residual capacity) And a discahrging pushes. There is a max of V discahrging pushes can be done on any edge. Hence max of 2V discahrging pushes. Pushes for an edge and its linked edge (numbered E) are O(VE). There are O(V^2) relabling operation called on V vertexes - O(V^3). Adding these O(V^3+VE) = O(V^3). (Since V<=E<=V^2 this conclusion is viable) ''' # initiates vars n = self.size goal = goal or self.sink origin = origin or self.source # inter_nodes, not sink or source, are viable for push inter_nodes = [i for i in range(n) if i != origin and i != goal] # pushes max capacity from source self.distance[origin] = n self.excess[origin] = float('inf') # resolves excess on source neighbors for v in range(n): self._push(origin, v) potential_vertex = 0 while potential_vertex < len(inter_nodes): u = inter_nodes[potential_vertex] # resolve ecxess flow from u. pred_distance = self.distance[u] self._discharge(u) # checks whether te vertex was relabled if self.distance[u] > pred_distance : # move to front selection rule # inserts the vertex to the front and set the search back to 0 inter_nodes.insert(0, inter_nodes.pop(potential_vertex)) potential_vertex = 0 else: potential_vertex += 1 return sum (self.flow[origin]) n = int(input()) m = int(input()) C = [[0] * (n+2) for _ in range(n+2)] source = 0 sink = n+1 for _m in range(m): user_inputs = input().split(" ") u = int(user_inputs[0]) v = int(user_inputs[1]) # if C[u][sink] != float("Inf"): C[source][u] = 1 # # if C[source][v] != float("Inf"): C[v][sink] = 1 C[u][v] = 1 C = np.array(C) graph = Graph(C) ed_result = graph.EdmondKarp(origin=source, goal=sink, data=True) max_flow = ed_result['max_flow'] # print(ed_result) result = dict() # print(ed_result['iteration'][0]) # print(ed_result['iteration'][max_flow-1][max_flow][1:-1]) # for u, v in zip(ed_result['iteration'][max_flow-1][max_flow][1:-1][0::2], ed_result['iteration'][max_flow-1][max_flow][1:-1][1::2]): # # print((u, v)) # result[u] = v # print(ed_result['iteration'][:-1]) # ed_result['iteration'][:-1].reverse() # print(list(reversed(ed_result['iteration'][:-1]))) # for i, val in enumerate(list(reversed(ed_result['iteration'][:-1]))): for i, val in enumerate(list(reversed(ed_result['iteration']))): # print(val) p = val[max_flow-i][1:-1] for u, v in zip(p[0::2], p[1::2]): if u not in result.keys(): result[u] = v result = sorted(result.items()) # print(result) print(max_flow) for u, v in result: print(f"{u} {v}") ```