Facebook Pixel

1820. Maximum Number of Accepted Invitations πŸ”’

Problem Description

You have a class with m boys and n girls who are attending an upcoming party. You need to determine the maximum number of boy-girl pairs that can be formed for the party.

You're given an m x n matrix called grid where each element is either 0 or 1. If grid[i][j] == 1, this means the i-th boy can invite the j-th girl to the party. If grid[i][j] == 0, the i-th boy cannot invite the j-th girl.

The matching rules are:

  • Each boy can invite at most one girl
  • Each girl can accept at most one invitation from a boy

Your task is to find the maximum number of successful invitations (matched pairs) possible.

For example, if you have 3 boys and 3 girls with a grid like:

[[1, 1, 0],
 [1, 0, 1],
 [0, 0, 1]]

Boy 0 can invite girls 0 or 1, boy 1 can invite girls 0 or 2, and boy 2 can only invite girl 2. The maximum number of matches would be 3 (boy 0 β†’ girl 1, boy 1 β†’ girl 0, boy 2 β†’ girl 2).

This is a classic bipartite matching problem where you need to find the maximum number of edges in a matching between two disjoint sets (boys and girls), with the constraint that no vertex is connected to more than one edge in the matching.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a bipartite graph where boys form one set of vertices and girls form another set. The edges represent possible invitations (when grid[i][j] == 1).

Is it a tree?

  • No: The graph is not a tree. It's a bipartite graph where multiple boys can potentially invite the same girl, and vice versa, creating a more complex structure than a tree.

Is the problem related to directed acyclic graphs?

  • No: While we could model this as a directed graph (invitations go from boys to girls), the problem is fundamentally about finding maximum matching in a bipartite graph, not about DAG properties.

Is the problem related to shortest paths?

  • No: We're not looking for shortest paths between nodes. We're looking for maximum matching.

Does the problem involve connectivity?

  • No: We're not checking if nodes are connected or finding connected components. We're finding the maximum number of valid pairings.

Does the problem have small constraints?

  • Yes: The constraints are typically small enough (usually m, n ≀ 100 or similar) that we can use algorithms with higher complexity like the Hungarian algorithm which uses DFS to find augmenting paths.

DFS/backtracking?

  • Yes: The solution uses DFS to find augmenting paths in the bipartite graph. Each DFS call tries to find an unmatched girl or a girl whose current match can be reassigned to create a new valid matching.

Conclusion: The flowchart leads us to use DFS/backtracking approach. The Hungarian algorithm implemented in the solution uses DFS to repeatedly find augmenting paths - paths that start from an unmatched boy and end at an unmatched girl (or a girl whose current partner can find another match). This DFS-based approach efficiently solves the maximum bipartite matching problem.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think of this problem like a matchmaking scenario at a dance party. We have boys who want to invite girls, but each person can only be paired with one partner. The challenge is to maximize the total number of pairs.

The key insight is recognizing this as a bipartite matching problem. We have two distinct groups (boys and girls), and we want to create the maximum number of one-to-one connections between them.

Why does the DFS approach work here? Let's break down the intuition:

  1. Start Simple: For each boy, we try to find a girl he can invite. If we find an available girl (not yet matched), great! We make the match.

  2. The Clever Part - Augmenting Paths: What if all girls a boy can invite are already matched? This is where DFS shines. Instead of giving up, we ask: "Can we rearrange existing matches to make room for this boy?"

  3. Chain Reaction: When boy i wants to invite girl j who's already matched with boy k, we use DFS to check if boy k can find another girl. If he can, we create a chain reaction:

    • Boy k switches to a different girl
    • This frees up girl j
    • Now boy i can invite girl j
  4. Why This Works: Each DFS call tries to find what's called an "augmenting path" - a path that increases the total number of matches by 1. We keep finding these paths until no more exist, guaranteeing we've found the maximum matching.

The vis set in the code prevents us from going in circles during each DFS attempt, ensuring we don't try the same girl twice in one matching attempt. The match array keeps track of which boy each girl is currently paired with (-1 means unpaired).

This greedy approach of trying to match each boy one by one, combined with the flexibility to rearrange previous matches through DFS, ensures we find the optimal solution without having to explore all possible combinations explicitly.

Learn more about Depth-First Search and Graph patterns.

Solution Approach

The solution implements the Hungarian Algorithm for maximum bipartite matching. Let's walk through the implementation step by step:

Data Structures Used:

  • match: An array of size n where match[j] stores which boy is currently matched with girl j. Initially set to -1 (unmatched).
  • vis: A set that tracks which girls have been visited during the current DFS attempt to avoid cycles.

Algorithm Implementation:

  1. Main Loop Structure:

    for i in range(m):
        vis = set()
        ans += find(i)

    For each boy i, we attempt to find a match. We reset the vis set for each boy to allow exploring all possible augmenting paths.

  2. The DFS Function (find):

    def find(i):
        for j, v in enumerate(grid[i]):
            if v and j not in vis:

    This function tries to find a match for boy i. It iterates through all girls j that boy i can invite (grid[i][j] == 1) and haven't been visited in this DFS path.

  3. Marking Visited:

    vis.add(j)

    We mark girl j as visited to prevent revisiting her in the same DFS path, avoiding infinite loops.

  4. The Core Matching Logic:

    if match[j] == -1 or find(match[j]):
        match[j] = i
        return True

    This is where the magic happens:

    • Case 1: match[j] == -1 - Girl j is unmatched. We can directly pair her with boy i.
    • Case 2: find(match[j]) - Girl j is already matched with some boy match[j]. We recursively try to find an alternative match for boy match[j]. If successful, we can "steal" girl j for boy i.
  5. Building the Answer: The algorithm processes boys one by one. Each successful find(i) call increases the total matches by 1. The sum of all successful matches gives us the maximum number of accepted invitations.

Why This Works:

The algorithm guarantees finding the maximum matching because:

  • It systematically explores all augmenting paths (paths that can increase the matching size)
  • The DFS ensures we explore all possible rearrangements of existing matches
  • Processing boys sequentially and maintaining the match array ensures we never decrease the matching size
  • When no more augmenting paths exist, we've reached the maximum matching

Time Complexity:

  • Each DFS can visit up to n girls: O(n)
  • We run DFS for each of the m boys: O(m)
  • Total: O(m Γ— n Γ— n) in the worst case, though typically much better in practice

The elegance of this solution lies in its simplicity - just 15 lines of code to solve a complex matching problem optimally!

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the DFS-based bipartite matching works.

Example Grid:

grid = [[1, 1, 0],
        [0, 1, 1],
        [1, 0, 0]]

This means:

  • Boy 0 can invite girls 0 or 1
  • Boy 1 can invite girls 1 or 2
  • Boy 2 can invite only girl 0

Initial State:

  • match = [-1, -1, -1] (all girls unmatched)
  • ans = 0

Step 1: Process Boy 0

  • vis = {}
  • Try to find a match for boy 0
  • Check girl 0: grid[0][0] = 1, girl 0 not visited
    • Add girl 0 to vis: vis = {0}
    • match[0] == -1 (girl 0 is unmatched)
    • Set match[0] = 0 (pair boy 0 with girl 0)
    • Return True
  • ans = 1, match = [0, -1, -1]

Step 2: Process Boy 1

  • vis = {} (reset for new boy)
  • Try to find a match for boy 1
  • Skip girl 0: grid[1][0] = 0
  • Check girl 1: grid[1][1] = 1, girl 1 not visited
    • Add girl 1 to vis: vis = {1}
    • match[1] == -1 (girl 1 is unmatched)
    • Set match[1] = 1 (pair boy 1 with girl 1)
    • Return True
  • ans = 2, match = [0, 1, -1]

Step 3: Process Boy 2

  • vis = {} (reset for new boy)
  • Try to find a match for boy 2
  • Check girl 0: grid[2][0] = 1, girl 0 not visited
    • Add girl 0 to vis: vis = {0}
    • match[0] == 0 (girl 0 is already matched with boy 0)
    • Recursively call find(0) to see if boy 0 can find another match:
      • In the recursive call for boy 0:
        • Skip girl 0: already in vis = {0}
        • Check girl 1: grid[0][1] = 1, girl 1 not in vis
        • Add girl 1 to vis: vis = {0, 1}
        • match[1] == 1 (girl 1 is matched with boy 1)
        • Recursively call find(1) to see if boy 1 can find another match:
          • In the recursive call for boy 1:
            • Skip girl 0: grid[1][0] = 0
            • Skip girl 1: already in vis = {0, 1}
            • Check girl 2: grid[1][2] = 1, girl 2 not in vis
            • Add girl 2 to vis: vis = {0, 1, 2}
            • match[2] == -1 (girl 2 is unmatched)
            • Set match[2] = 1 (boy 1 switches to girl 2)
            • Return True
        • Since boy 1 found alternative (girl 2), set match[1] = 0 (boy 0 takes girl 1)
        • Return True
    • Since boy 0 found alternative (girl 1), set match[0] = 2 (boy 2 takes girl 0)
    • Return True
  • ans = 3, match = [2, 0, 1]

Final Result:

  • Maximum matches: 3
  • Final pairing:
    • Boy 2 β†’ Girl 0
    • Boy 0 β†’ Girl 1
    • Boy 1 β†’ Girl 2

The key insight here is the chain reaction in Step 3: Boy 2 wanted girl 0, but she was taken. Through DFS, we found that if boy 0 switches to girl 1, and boy 1 switches to girl 2, then boy 2 can have girl 0. This cascading rearrangement is what makes the algorithm find the optimal solution!

Solution Implementation

1class Solution:
2    def maximumInvitations(self, grid: List[List[int]]) -> int:
3        """
4        Solves the maximum bipartite matching problem using Hungarian algorithm.
5        Each boy (row) can be matched with at most one girl (column) based on grid preferences.
6      
7        Args:
8            grid: 2D list where grid[i][j] = 1 means boy i can invite girl j
9      
10        Returns:
11            Maximum number of successful invitations (matches)
12        """
13      
14        def dfs_augmenting_path(boy_idx: int) -> bool:
15            """
16            Attempts to find an augmenting path starting from boy_idx using DFS.
17          
18            Args:
19                boy_idx: Index of the current boy trying to find a match
20          
21            Returns:
22                True if an augmenting path is found, False otherwise
23            """
24            # Try each girl that this boy can invite
25            for girl_idx, can_invite in enumerate(grid[boy_idx]):
26                # Check if boy can invite this girl and she hasn't been visited in this round
27                if can_invite and girl_idx not in visited_girls:
28                    # Mark this girl as visited to avoid cycles
29                    visited_girls.add(girl_idx)
30                  
31                    # If girl is unmatched or we can reassign her current match
32                    if girl_to_boy_match[girl_idx] == -1 or dfs_augmenting_path(girl_to_boy_match[girl_idx]):
33                        # Update the matching: assign this girl to current boy
34                        girl_to_boy_match[girl_idx] = boy_idx
35                        return True
36          
37            return False
38      
39        # Get dimensions: m boys and n girls
40        num_boys, num_girls = len(grid), len(grid[0])
41      
42        # Initialize matching array: girl_to_boy_match[j] = i means girl j is matched with boy i
43        # -1 means the girl is unmatched
44        girl_to_boy_match = [-1] * num_girls
45      
46        # Count total successful matches
47        total_matches = 0
48      
49        # Try to find a match for each boy
50        for boy_idx in range(num_boys):
51            # Reset visited set for each boy's matching attempt
52            visited_girls = set()
53          
54            # If we find an augmenting path, increment the match count
55            if dfs_augmenting_path(boy_idx):
56                total_matches += 1
57      
58        return total_matches
59
1class Solution {
2    // Grid representing the bipartite graph (boys to girls preferences)
3    private int[][] grid;
4    // Visited array to track girls visited in current DFS path
5    private boolean[] visited;
6    // Array to store which boy is matched to each girl (-1 if unmatched)
7    private int[] girlToBoymatch;
8    // Number of girls (columns in grid)
9    private int numGirls;
10
11    /**
12     * Finds maximum number of invitations using Hungarian algorithm / Maximum Bipartite Matching
13     * @param grid 2D array where grid[i][j] = 1 means boy i can invite girl j
14     * @return Maximum number of successful invitations
15     */
16    public int maximumInvitations(int[][] grid) {
17        int numBoys = grid.length;
18        numGirls = grid[0].length;
19        this.grid = grid;
20      
21        // Initialize visited array for DFS traversal
22        visited = new boolean[numGirls];
23      
24        // Initialize matching array, -1 means girl is unmatched
25        girlToBoymatch = new int[numGirls];
26        Arrays.fill(girlToBoymatch, -1);
27      
28        int maxMatches = 0;
29      
30        // Try to find an augmenting path for each boy
31        for (int boy = 0; boy < numBoys; boy++) {
32            // Reset visited array for each boy's DFS
33            Arrays.fill(visited, false);
34          
35            // If we can find an augmenting path starting from this boy
36            if (find(boy)) {
37                maxMatches++;
38            }
39        }
40      
41        return maxMatches;
42    }
43
44    /**
45     * DFS to find an augmenting path starting from given boy
46     * @param boy The boy index to find a match for
47     * @return true if an augmenting path is found, false otherwise
48     */
49    private boolean find(int boy) {
50        // Try to match with each girl
51        for (int girl = 0; girl < numGirls; girl++) {
52            // Check if boy can invite this girl and girl hasn't been visited in current path
53            if (grid[boy][girl] == 1 && !visited[girl]) {
54                // Mark girl as visited to avoid cycles in current DFS
55                visited[girl] = true;
56              
57                // If girl is unmatched OR we can reassign her current match to another girl
58                if (girlToBoymatch[girl] == -1 || find(girlToBoymatch[girl])) {
59                    // Match this boy with this girl
60                    girlToBoymatch[girl] = boy;
61                    return true;
62                }
63            }
64        }
65      
66        // No augmenting path found
67        return false;
68    }
69}
70
1class Solution {
2public:
3    int maximumInvitations(vector<vector<int>>& grid) {
4        int numBoys = grid.size();
5        int numGirls = grid[0].size();
6      
7        // visited[j] tracks if girl j has been visited in current DFS path
8        bool visited[210];
9      
10        // girlMatch[j] stores which boy is matched to girl j (-1 if unmatched)
11        int girlMatch[210];
12        memset(girlMatch, -1, sizeof(girlMatch));
13      
14        int maxMatches = 0;
15      
16        // Hungarian algorithm: find augmenting path starting from boy i
17        function<bool(int)> findAugmentingPath = [&](int boy) -> bool {
18            // Try to match current boy with each girl he's interested in
19            for (int girl = 0; girl < numGirls; ++girl) {
20                // Check if boy is interested in girl and girl hasn't been visited in this path
21                if (grid[boy][girl] && !visited[girl]) {
22                    visited[girl] = true;
23                  
24                    // If girl is unmatched or we can find an augmenting path 
25                    // from the boy currently matched to this girl
26                    if (girlMatch[girl] == -1 || findAugmentingPath(girlMatch[girl])) {
27                        girlMatch[girl] = boy;  // Match current boy with this girl
28                        return true;
29                    }
30                }
31            }
32            return false;  // No augmenting path found
33        };
34      
35        // Try to find a match for each boy
36        for (int boy = 0; boy < numBoys; ++boy) {
37            // Reset visited array for each new boy (new DFS search)
38            memset(visited, 0, sizeof(visited));
39          
40            // If we find an augmenting path, increment the match count
41            if (findAugmentingPath(boy)) {
42                maxMatches++;
43            }
44        }
45      
46        return maxMatches;
47    }
48};
49
1function maximumInvitations(grid: number[][]): number {
2    const numBoys = grid.length;
3    const numGirls = grid[0].length;
4  
5    // girlMatch[j] stores which boy is matched to girl j (-1 if unmatched)
6    const girlMatch: number[] = new Array(numGirls).fill(-1);
7  
8    // visited[j] tracks if girl j has been visited in current DFS path
9    let visited: boolean[] = [];
10  
11    let maxMatches = 0;
12  
13    // Hungarian algorithm: find augmenting path starting from boy
14    const findAugmentingPath = (boy: number): boolean => {
15        // Try to match current boy with each girl he's interested in
16        for (let girl = 0; girl < numGirls; girl++) {
17            // Check if boy is interested in girl and girl hasn't been visited in this path
18            if (grid[boy][girl] === 1 && !visited[girl]) {
19                visited[girl] = true;
20              
21                // If girl is unmatched or we can find an augmenting path 
22                // from the boy currently matched to this girl
23                if (girlMatch[girl] === -1 || findAugmentingPath(girlMatch[girl])) {
24                    girlMatch[girl] = boy;  // Match current boy with this girl
25                    return true;
26                }
27            }
28        }
29        return false;  // No augmenting path found
30    };
31  
32    // Try to find a match for each boy
33    for (let boy = 0; boy < numBoys; boy++) {
34        // Reset visited array for each new boy (new DFS search)
35        visited = new Array(numGirls).fill(false);
36      
37        // If we find an augmenting path, increment the match count
38        if (findAugmentingPath(boy)) {
39            maxMatches++;
40        }
41    }
42  
43    return maxMatches;
44}
45

Time and Space Complexity

Time Complexity: O(mΒ² Γ— n)

The algorithm implements the Hungarian algorithm (bipartite matching using augmenting paths). For each of the m boys (rows), we call the find function which attempts to find an augmenting path. In the worst case, find can recursively call itself up to m times (when it needs to reassign previously matched girls). Within each find call, we iterate through all n girls (columns). Therefore, the overall time complexity is O(m Γ— m Γ— n) = O(mΒ² Γ— n).

Note: The reference answer states O(m Γ— n), which would be the complexity if we only considered the iteration through the grid without accounting for the recursive depth of the augmenting path search. However, the actual worst-case complexity for this bipartite matching algorithm is O(mΒ² Γ— n).

Space Complexity: O(m + n)

The space complexity consists of:

  • match array of size n: O(n)
  • vis set which can contain at most n elements: O(n)
  • Recursion stack depth which can go up to m in the worst case: O(m)

Therefore, the total space complexity is O(m + n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Resetting the Visited Set for Each Boy

The Pitfall: A common mistake is to use a global visited set that doesn't get reset between different boys' matching attempts:

# WRONG APPROACH
def maximumInvitations(self, grid):
    visited_girls = set()  # Global visited set - WRONG!
  
    def dfs_augmenting_path(boy_idx):
        for girl_idx, can_invite in enumerate(grid[boy_idx]):
            if can_invite and girl_idx not in visited_girls:
                visited_girls.add(girl_idx)
                # ... rest of logic
  
    for boy_idx in range(num_boys):
        # visited_girls never gets reset!
        if dfs_augmenting_path(boy_idx):
            total_matches += 1

Why This Fails:

  • Once a girl is visited while finding a match for one boy, she becomes permanently marked as visited
  • Subsequent boys cannot explore paths through that girl, even though she might be part of their optimal augmenting path
  • This leads to suboptimal matching results

Example Where It Breaks:

Grid: [[1, 1],
       [0, 1]]

With global visited:
- Boy 0 tries girl 0 (marks visited), gets matched
- Boy 0 tries girl 1 (marks visited), but girl 1 is harder to match
- Boy 1 can only invite girl 1, but she's already visited!
- Result: Only 1 match instead of 2

The Solution: Always reset the visited set for each boy's matching attempt:

for boy_idx in range(num_boys):
    visited_girls = set()  # Reset for each boy - CORRECT!
    if dfs_augmenting_path(boy_idx):
        total_matches += 1

2. Confusing Match Direction (Boy-to-Girl vs Girl-to-Boy)

The Pitfall: Mixing up the matching array's direction can lead to incorrect implementations:

# WRONG: Using boy_to_girl_match instead of girl_to_boy_match
boy_to_girl_match = [-1] * num_boys  # WRONG direction!

def dfs_augmenting_path(boy_idx):
    for girl_idx, can_invite in enumerate(grid[boy_idx]):
        if can_invite and girl_idx not in visited_girls:
            visited_girls.add(girl_idx)
            # This logic doesn't work with boy_to_girl array
            if boy_to_girl_match[boy_idx] == -1:  # WRONG!
                boy_to_girl_match[boy_idx] = girl_idx
                return True

Why This Fails:

  • We need to track which boy each girl is matched with (not vice versa)
  • This allows us to check if a girl is available and potentially reassign her current match
  • Using boy-to-girl matching makes it impossible to check if a specific girl is already matched

The Solution: Use girl_to_boy_match array where index represents the girl and value represents the matched boy:

girl_to_boy_match = [-1] * num_girls  # CORRECT!
# girl_to_boy_match[j] = i means girl j is matched with boy i

3. Forgetting to Check Grid Boundaries

The Pitfall: Assuming the grid is always non-empty or square:

# WRONG: No validation
def maximumInvitations(self, grid):
    num_boys, num_girls = len(grid), len(grid[0])  # Crashes if grid is empty!

The Solution: Add proper boundary checks:

def maximumInvitations(self, grid):
    if not grid or not grid[0]:
        return 0
    num_boys, num_girls = len(grid), len(grid[0])

4. Not Understanding When to Return True in DFS

The Pitfall: Returning True immediately when finding any available girl, without trying to reassign:

# WRONG: Greedy approach without reassignment
def dfs_augmenting_path(boy_idx):
    for girl_idx, can_invite in enumerate(grid[boy_idx]):
        if can_invite and girl_to_boy_match[girl_idx] == -1:
            girl_to_boy_match[girl_idx] = boy_idx
            return True  # WRONG: Not trying to reassign already matched girls
    return False

Why This Fails: This greedy approach might miss optimal solutions where reassigning existing matches leads to more total matches.

The Solution: Always attempt reassignment through recursive DFS:

if girl_to_boy_match[girl_idx] == -1 or dfs_augmenting_path(girl_to_boy_match[girl_idx]):
    girl_to_boy_match[girl_idx] = boy_idx
    return True
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More