Place as many applicants as possible
There are N applicants and M jobs. matrix[i][j] is 1 if
applicant i is qualified for job j and 0 otherwise. Each job can be given to
at most one applicant and each applicant can take at most one job. Return the largest number of applicants who
can be given a job they are qualified for.
Examples
| Input | Output | Why |
|---|---|---|
matrix = [[1,1,0,0],[1,0,1,0],[0,1,1,0],[0,0,1,1]] | 4 | For example 0→job 1, 1→job 0, 2→job 2, 3→job 3. |
matrix = [[1,1,1],[1,0,0],[1,0,0]] | 2 | Applicants 1 and 2 both want only job 0; one of them must miss out. |
matrix = [[1],[1],[1]] | 1 | One job. |
Constraints
- 1 ≤ N, M ≤ 200
- matrix[i][j] is 0 or 1
Hints
Hint 1
Give each applicant a job in turn. If a job they want is taken, ask whether its holder could move to another job they qualify for.
Hint 2
That question is recursive: the holder may need to displace someone else. Track which jobs have been tried in this round so the search ends.
Hint 3
Each applicant who finds such a chain adds one to the matching. The chain is an augmenting path.
Where people lose marks
- Greedily giving each applicant the first free job and never reassigning: example 1 then places only three.
- Sharing one tried set across all applicants instead of resetting it for each.
- Assuming N = M: the matrix may be rectangular.
Dry run on [[1,1,0,0],[1,0,1,0],[0,1,1,0],[0,0,1,1]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 4matched1
A1: augmenting path A1–J1. J1 was free. 1 matched.
-
Step 2 / 4matched2
A2: augmenting path A2–J1, A1–J2. To make room, A1 moves from J1 to J2. 2 matched.
-
Step 3 / 4matched3
A3: augmenting path A3–J2, A1–J1, A2–J3. To make room, A1 moves from J2 to J1; A2 moves from J1 to J3. 3 matched.
-
Step 4 / 4matched4
A4: augmenting path A4–J4. J4 was free. 4 matched. No more augmenting paths: the maximum matching has 4 pairs.
How to think about it
Process applicants one at a time and try to extend the matching by one with an augmenting path:
tryAssign(a)looks at each job a qualifies for that has not been tried in this round. If the job is free, or its current holder can be reassigned by a recursivetryAssign, the job goes to a.- A fresh
triedset per applicant makes each search visit every job at most once.
If no augmenting path exists for an applicant now, none will appear later, so a single pass over the applicants finds a maximum matching (Berge's theorem). This is the flow algorithm of the previous module, specialised to capacities of 1.
Complexity. Time O(N · N · M) · Space O(N + M)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
import sys
def max_matching(matrix):
sys.setrecursionlimit(10000)
n, m = len(matrix), len(matrix[0])
holder = [-1] * m # holder[j]: applicant currently in job j
def try_assign(a, tried):
for j in range(m):
if matrix[a][j] and not tried[j]:
tried[j] = True
if holder[j] == -1 or try_assign(holder[j], tried):
holder[j] = a # augmenting path found: a takes job j
return True
return False
return sum(1 for a in range(n) if try_assign(a, [False] * m))#include <functional>
#include <vector>
int solve(const std::vector<std::vector<int>>& matrix) {
const int n = matrix.size(), m = matrix[0].size();
std::vector<int> holder(m, -1); // applicant in each job
std::vector<bool> tried;
std::function<bool(int)> tryAssign = [&](int a) {
for (int j = 0; j < m; ++j) {
if (!matrix[a][j] || tried[j]) continue;
tried[j] = true;
if (holder[j] == -1 || tryAssign(holder[j])) { holder[j] = a; return true; }
}
return false;
};
int placed = 0;
for (int a = 0; a < n; ++a) {
tried.assign(m, false); // a fresh search per applicant
if (tryAssign(a)) ++placed;
}
return placed;
}class Solution {
private int[][] matrix;
private int[] holder;
private boolean[] tried;
private boolean tryAssign(int a) {
for (int j = 0; j < holder.length; j++) {
if (matrix[a][j] == 0 || tried[j]) continue;
tried[j] = true;
if (holder[j] == -1 || tryAssign(holder[j])) { holder[j] = a; return true; }
}
return false;
}
public int maxMatching(int[][] matrix) {
this.matrix = matrix;
holder = new int[matrix[0].length];
java.util.Arrays.fill(holder, -1); // applicant in each job
int placed = 0;
for (int a = 0; a < matrix.length; a++) {
tried = new boolean[holder.length]; // a fresh search per applicant
if (tryAssign(a)) placed++;
}
return placed;
}
}function maxMatching(matrix) {
const n = matrix.length, m = matrix[0].length;
const holder = new Array(m).fill(-1); // applicant in each job
const tryAssign = (a, tried) => {
for (let j = 0; j < m; j++) {
if (!matrix[a][j] || tried[j]) continue;
tried[j] = true;
if (holder[j] === -1 || tryAssign(holder[j], tried)) { holder[j] = a; return true; }
}
return false;
};
let placed = 0;
for (let a = 0; a < n; a++) if (tryAssign(a, new Array(m).fill(false))) placed++;
return placed;
}fn solve<const M: usize>(matrix: &[[i64; M]]) -> i64 {
fn try_assign<const M: usize>(a: usize, matrix: &[[i64; M]], holder: &mut Vec<Option<usize>>, tried: &mut Vec<bool>) -> bool {
for j in 0..M {
if matrix[a][j] == 0 || tried[j] {
continue;
}
tried[j] = true;
let free = match holder[j] {
None => true,
Some(h) => try_assign(h, matrix, holder, tried),
};
if free {
holder[j] = Some(a); // a takes job j
return true;
}
}
false
}
let mut holder = vec![None; M];
let mut placed = 0;
for a in 0..matrix.len() {
let mut tried = vec![false; M]; // a fresh search per applicant
if try_assign(a, matrix, &mut holder, &mut tried) {
placed += 1;
}
}
placed
}import qualified Data.Map.Strict as M
import qualified Data.Set as S
solve :: [[Int]] -> Int
solve matrix = fst (foldl place (0, M.empty) [0 .. length matrix - 1])
where
wants a = [j | (j, 1) <- zip [0 ..] (matrix !! a)]
-- the state is (number placed, map from job to its applicant)
place (k, holder) a = case tryAssign a S.empty holder of
Right holder' -> (k + 1, holder')
Left _ -> (k, holder)
-- Right: the holders after an augmenting path; Left: the jobs tried, shared across the search
tryAssign a tried holder = go (wants a) tried
where
go [] tr = Left tr
go (j : js) tr
| S.member j tr = go js tr
| otherwise = case M.lookup j holder of
Nothing -> Right (M.insert j a holder)
Just h -> case tryAssign h (S.insert j tr) holder of
Right holder' -> Right (M.insert j a holder')
Left tr' -> go js tr'
Variants to try
- Return the assignment itself: job → applicant, from the match array.
- Each job has room for k people: give each job k slots, or use a flow network with capacity k into the sink.
- Applicants rank jobs and the total rank must be minimised: that is the assignment problem (the Hungarian algorithm).
Next problems
Count the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Easy12 minDoes an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem →Write a solution and run it against the real test table.
Keep going
Count the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Easy12 minDoes an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.