Medium

Maximum Number of Accepted InvitationsPython

Full explanation · Time O(m * n * sqrt(m + n)) · Space O(m + n)

# Time:  O(m * n * sqrt(m + n))
# Space: O(m * n)

from functools import partial

# Time:  O(E * sqrt(V))
# Space: O(V)
# Source code from http://code.activestate.com/recipes/123641-hopcroft-karp-bipartite-matching/
# Hopcroft-Karp bipartite max-cardinality matching and max independent set
# David Eppstein, UC Irvine, 27 Apr 2002
def bipartiteMatch(graph):
    '''Find maximum cardinality matching of a bipartite graph (U,V,E).
    The input format is a dictionary mapping members of U to a list
    of their neighbors in V.  The output is a triple (M,A,B) where M is a
    dictionary mapping members of V to their matches in U, A is the part
    of the maximum independent set in U, and B is the part of the MIS in V.
    The same object may occur in both U and V, and is treated as two
    distinct vertices if this happens.'''
    
    # initialize greedy matching (redundant, but faster than full search)
    matching = {}
    for u in graph:
        for v in graph[u]:
            if v not in matching:
                matching[v] = u
                break
    
    while 1:
        # structure residual graph into layers
        # pred[u] gives the neighbor in the previous layer for u in U
        # preds[v] gives a list of neighbors in the previous layer for v in V
        # unmatched gives a list of unmatched vertices in final layer of V,
        # and is also used as a flag value for pred[u] when u is in the first layer
        preds = {}
        unmatched = []
        pred = dict([(u,unmatched) for u in graph])
        for v in matching:
            del pred[matching[v]]
        layer = list(pred)
        
        # repeatedly extend layering structure by another pair of layers
        while layer and not unmatched:
            newLayer = {}
            for u in layer:
                for v in graph[u]:
                    if v not in preds:
                        newLayer.setdefault(v,[]).append(u)
            layer = []
            for v in newLayer:
                preds[v] = newLayer[v]
                if v in matching:
                    layer.append(matching[v])
                    pred[matching[v]] = v
                else:
                    unmatched.append(v)
        
        # did we finish layering without finding any alternating paths?
        if not unmatched:
            unlayered = {}
            for u in graph:
                for v in graph[u]:
                    if v not in preds:
                        unlayered[v] = None
            return (matching,list(pred),list(unlayered))

        # recursively search backward through layers to find alternating paths
        # recursion returns true if found path, false otherwise
        def recurse(v):
            if v in preds:
                L = preds[v]
                del preds[v]
                for u in L:
                    if u in pred:
                        pu = pred[u]
                        del pred[u]
                        if pu is unmatched or recurse(pu):
                            matching[v] = u
                            return 1
            return 0
        
        def recurse_iter(v):
            def divide(v):
                if v not in preds:
                    return
                L = preds[v]
                del preds[v]
                for u in L :
                    if u in pred and pred[u] is unmatched:  # early return
                        del pred[u]
                        matching[v] = u
                        ret[0] = True
                        return
                stk.append(partial(conquer, v, iter(L)))

            def conquer(v, it):
                for u in it:
                    if u not in pred:
                        continue
                    pu = pred[u]
                    del pred[u]
                    stk.append(partial(postprocess, v, u, it))
                    stk.append(partial(divide, pu))
                    return

            def postprocess(v, u, it):
                if not ret[0]:
                    stk.append(partial(conquer, v, it))
                    return
                matching[v] = u

            ret, stk = [False], []
            stk.append(partial(divide, v))
            while stk:
                stk.pop()()
            return ret[0]

        for v in unmatched: recurse_iter(v)


import collections


# Hopcroft-Karp bipartite matching
class Solution(object):
    def maximumInvitations(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        adj = collections.defaultdict(list)
        for i in xrange(len(grid)):
            for j in xrange(len(grid[0])):
                if not grid[i][j]:
                    continue
                adj[j].append(i)
        return len(bipartiteMatch(adj)[0])


# Time:  O(|V| * |E|) = O(min(m, n) * (m * n))
# Space: O(|V|) = O(min(m, n))
# Hungarian bipartite matching with less space
class Solution2(object):
    def maximumInvitations(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        def augment(grid, u, lookup, match):
            for v in xrange(V):
                if not get_grid(u, v) or v in lookup:
                    continue
                lookup.add(v)
                if v not in match or augment(grid, match[v], lookup, match):
                    match[v] = u  # greedily match
                    return True
            return False
    
        def hungarian(grid):
            match = {}
            for i in xrange(U):
                augment(grid, i, set(), match)
            return len(match)

        U, V = min(len(grid), len(grid[0])), max(len(grid), len(grid[0]))
        get_grid = (lambda x, y: grid[x][y]) if len(grid) < len(grid[0]) else (lambda x, y: grid[y][x])
        return hungarian(grid)


# Time:  O(|V| * |E|) = O(min(m, n) * (m * n))
# Space: O(|E|) = O(m * n)
import collections


# Hungarian bipartite matching
class Solution3(object):
    def maximumInvitations(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        def augment(adj, u, lookup, match):
            for v in adj[u]:
                if v in lookup:
                    continue
                lookup.add(v)
                if v not in match or augment(adj, match[v], lookup, match):
                    match[v] = u  # greedily match
                    return True
            return False
    
        def hungarian(adj):
            match = {}
            for i in adj.iterkeys():
                augment(adj, i, set(), match)
            return len(match)
        
        adj = collections.defaultdict(list)
        for i in xrange(len(grid)):
            for j in xrange(len(grid[0])):
                if not grid[i][j]:
                    continue
                if len(grid) < len(grid[0]):
                    adj[i].append(j)
                else:
                    adj[j].append(i)
        return hungarian(adj)