---
# System prepended metadata

title: Bipartite Matching

---

# 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}")

```