1349. Maximum Students Taking Exam
Problem Description
You are given a m × n
matrix seats
representing the seat distribution in a classroom. Each seat is marked as:
'#'
if the seat is broken (cannot be used)'.'
if the seat is in good condition (can be used)
Students taking an exam have a specific cheating pattern - they can see the answers of other students sitting:
- Directly to their left
- Directly to their right
- Diagonally to their upper left
- Diagonally to their upper right
However, they cannot see the answers of students sitting directly in front or behind them.
Your task is to find the maximum number of students that can be seated in the classroom to take the exam such that no student can cheat from another. Students can only be placed in seats that are in good condition (marked with '.'
).
For example, if we have a classroom layout:
. # . . # . . . # . . .
We need to strategically place students so that:
- No two students sit next to each other in the same row (left/right adjacency)
- No student sits diagonally adjacent to another student in the row above (upper-left or upper-right positions)
- Students are only placed in good seats (marked with
'.'
)
The goal is to maximize the total number of students that can take the exam under these constraints.
Intuition
The key insight is that students in each row can only affect students in adjacent rows (the row above and below). Since students cannot see answers from those directly in front or behind them, we only need to worry about diagonal conflicts between consecutive rows.
This observation suggests we can process the classroom row by row. For each row, we need to decide which seats to fill while ensuring:
- No two students sit adjacent in the same row
- Students in the current row don't conflict with students in the previous row (diagonal positions)
Since each seat has only two states (occupied or empty), and the number of seats per row is relatively small (at most 8 based on typical constraints), we can use bit manipulation to represent seat arrangements. Each row's state can be encoded as a binary number where 1
means a student is seated and 0
means the seat is empty.
For example, if we have students in positions 0 and 3 of a row with 5 seats, we can represent this as 01001
(reading right to left in binary).
The problem now becomes: given the current row's available seats and the previous row's student arrangement, what's the maximum number of students we can place in all remaining rows?
This naturally leads to a dynamic programming approach where:
- State: current row index and the student arrangement of the current row
- Decision: try all valid student arrangements for the current row
- Transition: move to the next row with updated constraints based on current placement
We can use memoization to avoid recalculating the same states. The recursive function dfs(seat, i)
explores all valid ways to place students starting from row i
, where seat
represents which positions are available (considering both broken seats and diagonal conflicts from the previous row).
For each row, we enumerate all possible student arrangements (mask
) and check:
- Students are only placed in available seats:
(seat | mask) == seat
- No adjacent students in the same row:
!(mask & (mask << 1))
The beauty of this approach is that it systematically explores all valid configurations while efficiently pruning invalid ones through bit operations.
Learn more about Dynamic Programming and Bitmask patterns.
Solution Approach
The solution uses state compression with memoization search to efficiently explore all valid seating arrangements.
State Representation
First, we convert the classroom matrix into a compressed format. The function f(seat)
transforms each row into a bitmask where:
- Bit position
i
is set to1
if seati
is available (.
) - Bit position
i
is set to0
if seati
is broken (#
)
def f(seat: List[str]) -> int:
mask = 0
for i, c in enumerate(seat):
if c == '.':
mask |= 1 << i
return mask
This creates an array ss
where ss[i]
represents the available seats in row i
.
Dynamic Programming with Memoization
The core algorithm uses a recursive function dfs(seat, i)
that:
seat
: bitmask representing available positions in rowi
(considering broken seats and diagonal conflicts from previous row)i
: current row index- Returns: maximum students that can be seated from row
i
to the last row
Enumerating Valid Arrangements
For each row, we try all possible student arrangements from 0
to 2^n - 1
(where n
is the number of columns):
for mask in range(1 << n):
Validity Checks
For each arrangement mask
, we verify:
-
Students only in available seats:
(seat | mask) != seat
- If this condition is true, skip this arrangement
- This ensures we don't place students in broken seats or positions that conflict with the previous row
-
No adjacent students:
(mask & (mask << 1))
- If this is non-zero, students are sitting adjacent
mask << 1
shifts all bits left by 1 position- The AND operation detects if any student has a neighbor to their left
State Transition
If the arrangement is valid:
-
Count students in current row:
cnt = mask.bit_count()
-
If it's the last row (
i == len(ss) - 1
):- Update answer:
ans = max(ans, cnt)
- Update answer:
-
Otherwise, prepare the next row:
- Start with available seats:
nxt = ss[i + 1]
- Remove positions diagonally below current students:
nxt &= ~(mask << 1)
(removes upper-right diagonal conflicts)nxt &= ~(mask >> 1)
(removes upper-left diagonal conflicts)
- Recursively solve:
ans = max(ans, cnt + dfs(nxt, i + 1))
- Start with available seats:
Memoization
The @cache
decorator automatically memoizes the results of dfs(seat, i)
, preventing redundant calculations when the same state is encountered again.
Final Result
The algorithm starts with dfs(ss[0], 0)
, exploring all valid arrangements starting from the first row with all initially available seats, and returns the maximum number of students that can be seated.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this 2×3 classroom:
. . # # . .
Step 1: Convert to bitmasks
First, we convert each row to a bitmask (reading positions from right to left):
- Row 0:
. . #
→011
(binary) = 3 (decimal)- Position 0:
.
→ bit 0 = 1 - Position 1:
.
→ bit 1 = 1 - Position 2:
#
→ bit 2 = 0
- Position 0:
- Row 1:
# . .
→110
(binary) = 6 (decimal)
So ss = [3, 6]
Step 2: Start DFS from row 0
Call dfs(seat=3, i=0)
where seat=3 (011
) represents available positions in row 0.
Step 3: Try all possible arrangements for row 0
We enumerate masks from 0 to 7 (2³-1):
-
mask=0 (
000
): No students placed- Valid (no adjacency, all positions available)
- Count = 0
- Next row's available seats:
ss[1] = 6
(110
) - No diagonal conflicts to remove
- Recursively solve:
0 + dfs(6, 1)
-
mask=1 (
001
): Student at position 0- Check:
(3 | 1) = 3 == 3
✓ (only using available seats) - Check:
(1 & 2) = 0
✓ (no adjacency) - Count = 1
- Next row available:
6 & ~(1<<1) & ~(1>>1) = 6 & ~2 & ~0 = 4
(100
) - Removes position 1 from next row (diagonal conflict)
- Recursively solve:
1 + dfs(4, 1)
- Check:
-
mask=2 (
010
): Student at position 1- Valid checks pass
- Count = 1
- Next row available:
6 & ~(2<<1) & ~(2>>1) = 6 & ~4 & ~1 = 2
(010
) - Removes positions 0 and 2 from next row
- Recursively solve:
1 + dfs(2, 1)
-
mask=3 (
011
): Students at positions 0 and 1- Check:
(3 & 6) = 2
✗ (adjacent students!) - Skip this arrangement
- Check:
-
mask=4 (
100
): Student at position 2- Check:
(3 | 4) = 7 ≠ 3
✗ (trying to use broken seat!) - Skip this arrangement
- Check:
Step 4: Process row 1 recursively
For each valid arrangement from row 0, we solve row 1 (the last row).
For example, when row 0 has mask=1:
- Call
dfs(4, 1)
where seat=4 (100
) - Try mask=0: 0 students
- Try mask=4 (
100
): 1 student at position 2 (valid) - Maximum for this path: 1 (from row 0) + 1 (from row 1) = 2
Step 5: Combine results
After exploring all paths:
- Path 1: No students in row 0, optimal arrangement in row 1 → total = 2
- Path 2: Student at position 0 in row 0, student at position 2 in row 1 → total = 2
- Path 3: Student at position 1 in row 0, student at position 1 in row 1 → total = 2
Maximum students = 2
One optimal arrangement:
X . # (X = student placed) # . X
This satisfies all constraints:
- No adjacent students in any row
- No diagonal conflicts between rows
- Students only in good seats
Solution Implementation
1class Solution:
2 def maxStudents(self, seats: List[List[str]]) -> int:
3 def get_available_mask(row: List[str]) -> int:
4 """
5 Convert a row of seats to a bitmask where 1 means the seat is available ('.')
6 and 0 means the seat is broken ('#')
7 """
8 mask = 0
9 for position, seat in enumerate(row):
10 if seat == '.':
11 mask |= 1 << position
12 return mask
13
14 @cache
15 def dp(available_seats: int, row_index: int) -> int:
16 """
17 Dynamic programming function to find maximum students that can be seated
18 from current row to the last row.
19
20 Args:
21 available_seats: Bitmask of available seats in current row
22 row_index: Current row index
23
24 Returns:
25 Maximum number of students that can be seated from this row onwards
26 """
27 max_students = 0
28
29 # Try all possible seating arrangements for current row
30 for seating_mask in range(1 << num_cols):
31 # Check if seating arrangement is valid:
32 # 1. Students only sit in available seats (seating_mask is subset of available_seats)
33 # 2. No two students sit adjacent to each other (no consecutive 1s in mask)
34 if (available_seats | seating_mask) != available_seats or (seating_mask & (seating_mask << 1)):
35 continue
36
37 students_in_row = seating_mask.bit_count()
38
39 # Base case: last row
40 if row_index == len(seat_masks) - 1:
41 max_students = max(max_students, students_in_row)
42 else:
43 # Calculate available seats for next row considering current seating
44 # Students in current row block diagonal seats in next row
45 next_available = seat_masks[row_index + 1]
46 next_available &= ~(seating_mask << 1) # Block left diagonal
47 next_available &= ~(seating_mask >> 1) # Block right diagonal
48
49 # Recursively solve for next row
50 max_students = max(max_students, students_in_row + dp(next_available, row_index + 1))
51
52 return max_students
53
54 num_cols = len(seats[0])
55 # Convert each row to a bitmask representation
56 seat_masks = [get_available_mask(row) for row in seats]
57
58 # Start dynamic programming from first row
59 return dp(seat_masks[0], 0)
60
1class Solution {
2 // Memoization cache: dp[availableSeats][rowIndex] = maximum students
3 private Integer[][] dp;
4 // Number of columns in the classroom
5 private int numCols;
6 // Available seats for each row (bit mask: 1 = available seat, 0 = broken seat)
7 private int[] availableSeats;
8
9 public int maxStudents(char[][] seats) {
10 int numRows = seats.length;
11 numCols = seats[0].length;
12 availableSeats = new int[numRows];
13
14 // Initialize memoization cache
15 dp = new Integer[1 << numCols][numRows];
16
17 // Convert seat availability to bit masks for each row
18 // Bit i is 1 if seat at column i is available ('.')
19 for (int row = 0; row < numRows; ++row) {
20 for (int col = 0; col < numCols; ++col) {
21 if (seats[row][col] == '.') {
22 availableSeats[row] |= 1 << col;
23 }
24 }
25 }
26
27 // Start DFS from row 0 with all available seats in that row
28 return dfs(availableSeats[0], 0);
29 }
30
31 /**
32 * DFS with memoization to find maximum students that can be seated
33 * @param currentRowSeats Available seats in current row (considering conflicts from previous row)
34 * @param rowIndex Current row index being processed
35 * @return Maximum number of students that can be seated from this row onwards
36 */
37 private int dfs(int currentRowSeats, int rowIndex) {
38 // Return cached result if already computed
39 if (dp[currentRowSeats][rowIndex] != null) {
40 return dp[currentRowSeats][rowIndex];
41 }
42
43 int maxStudents = 0;
44
45 // Try all possible seating arrangements (masks) for current row
46 for (int seatMask = 0; seatMask < (1 << numCols); ++seatMask) {
47 // Check if this seating arrangement is valid:
48 // 1. All seated positions must be available seats
49 // 2. No two adjacent students in the same row
50 if ((currentRowSeats | seatMask) != currentRowSeats ||
51 (seatMask & (seatMask << 1)) != 0) {
52 continue;
53 }
54
55 // Count students in current seating arrangement
56 int studentsInRow = Integer.bitCount(seatMask);
57
58 if (rowIndex == availableSeats.length - 1) {
59 // Last row: just count students in this row
60 maxStudents = Math.max(maxStudents, studentsInRow);
61 } else {
62 // Not last row: calculate available seats for next row
63 // considering diagonal conflicts with current row's students
64 int nextRowSeats = availableSeats[rowIndex + 1];
65
66 // Remove seats that would have left-diagonal conflict
67 nextRowSeats &= ~(seatMask << 1);
68 // Remove seats that would have right-diagonal conflict
69 nextRowSeats &= ~(seatMask >> 1);
70
71 // Recursively solve for next row and add current row's students
72 maxStudents = Math.max(maxStudents,
73 studentsInRow + dfs(nextRowSeats, rowIndex + 1));
74 }
75 }
76
77 // Cache and return the result
78 return dp[currentRowSeats][rowIndex] = maxStudents;
79 }
80}
81
1class Solution {
2public:
3 int maxStudents(vector<vector<char>>& seats) {
4 int numRows = seats.size();
5 int numCols = seats[0].size();
6
7 // availableSeats[i] stores a bitmask of available seats in row i
8 // bit j is set to 1 if seat at column j in row i is available ('.')
9 vector<int> availableSeats(numRows);
10
11 // dp[mask][row] = maximum students that can be seated from row 'row' to last row
12 // when available seats in row 'row' are represented by 'mask'
13 vector<vector<int>> dp(1 << numCols, vector<int>(numRows, -1));
14
15 // Build bitmask for available seats in each row
16 for (int row = 0; row < numRows; ++row) {
17 for (int col = 0; col < numCols; ++col) {
18 if (seats[row][col] == '.') {
19 availableSeats[row] |= (1 << col);
20 }
21 }
22 }
23
24 // DFS with memoization to find maximum students
25 function<int(int, int)> dfs = [&](int currentAvailableSeats, int currentRow) -> int {
26 // Return memoized result if already computed
27 if (dp[currentAvailableSeats][currentRow] != -1) {
28 return dp[currentAvailableSeats][currentRow];
29 }
30
31 int maxStudentsCount = 0;
32
33 // Try all possible student arrangements for current row
34 for (int studentMask = 0; studentMask < (1 << numCols); ++studentMask) {
35 // Check if this arrangement is valid:
36 // 1. Students can only sit in available seats: (currentAvailableSeats | studentMask) == currentAvailableSeats
37 // 2. No two students sit next to each other: (studentMask & (studentMask << 1)) == 0
38 if ((currentAvailableSeats | studentMask) != currentAvailableSeats ||
39 (studentMask & (studentMask << 1)) != 0) {
40 continue;
41 }
42
43 // Count students in current arrangement
44 int studentsInCurrentRow = __builtin_popcount(studentMask);
45
46 if (currentRow == numRows - 1) {
47 // Last row: just count students in this row
48 maxStudentsCount = max(maxStudentsCount, studentsInCurrentRow);
49 } else {
50 // Not last row: calculate available seats for next row
51 // Students in current row block diagonal seats in next row
52 int nextRowAvailableSeats = availableSeats[currentRow + 1];
53 nextRowAvailableSeats &= ~(studentMask >> 1); // Block left diagonal
54 nextRowAvailableSeats &= ~(studentMask << 1); // Block right diagonal
55
56 // Recursively solve for next row
57 maxStudentsCount = max(maxStudentsCount,
58 studentsInCurrentRow + dfs(nextRowAvailableSeats, currentRow + 1));
59 }
60 }
61
62 // Memoize and return result
63 return dp[currentAvailableSeats][currentRow] = maxStudentsCount;
64 };
65
66 // Start DFS from first row with all available seats
67 return dfs(availableSeats[0], 0);
68 }
69};
70
1/**
2 * Calculates the maximum number of students that can sit in the exam room
3 * without cheating (no adjacent seats horizontally or diagonally)
4 * @param seats - 2D array where '.' represents available seat and '#' represents broken seat
5 * @returns Maximum number of students that can be seated
6 */
7function maxStudents(seats: string[][]): number {
8 const numRows: number = seats.length;
9 const numCols: number = seats[0].length;
10
11 // Bitmask array representing available seats for each row
12 // If seat[i][j] is available, bit j is set to 1
13 const availableSeats: number[] = Array(numRows).fill(0);
14
15 // Memoization table: dp[seatMask][rowIndex] stores the maximum students
16 // that can be seated from row i to the last row given the available seats
17 const memoTable: number[][] = Array.from(
18 { length: 1 << numCols },
19 () => Array(numRows).fill(-1)
20 );
21
22 // Convert seat availability to bitmask representation for each row
23 for (let row = 0; row < numRows; ++row) {
24 for (let col = 0; col < numCols; ++col) {
25 if (seats[row][col] === '.') {
26 availableSeats[row] |= 1 << col;
27 }
28 }
29 }
30
31 /**
32 * DFS function to calculate maximum students that can be seated
33 * @param availableSeatMask - Bitmask of available seats for current row
34 * @param currentRow - Current row index being processed
35 * @returns Maximum students that can be seated from current row to last row
36 */
37 const dfs = (availableSeatMask: number, currentRow: number): number => {
38 // Return memoized result if already calculated
39 if (memoTable[availableSeatMask][currentRow] !== -1) {
40 return memoTable[availableSeatMask][currentRow];
41 }
42
43 let maxStudentsCount: number = 0;
44
45 // Try all possible seating arrangements for current row
46 for (let seatingMask = 0; seatingMask < (1 << numCols); ++seatingMask) {
47 // Check if seating arrangement is valid:
48 // 1. All seated positions must be available seats
49 // 2. No two students can sit next to each other horizontally
50 if ((availableSeatMask | seatingMask) !== availableSeatMask ||
51 (seatingMask & (seatingMask << 1)) !== 0) {
52 continue;
53 }
54
55 // Count number of students in current seating arrangement
56 const studentsInCurrentRow: number = seatingMask.toString(2).split('1').length - 1;
57
58 if (currentRow === numRows - 1) {
59 // Last row: just count students in this row
60 maxStudentsCount = Math.max(maxStudentsCount, studentsInCurrentRow);
61 } else {
62 // Calculate available seats for next row considering diagonal conflicts
63 let nextRowAvailableSeats: number = availableSeats[currentRow + 1];
64
65 // Remove seats that would be diagonally adjacent to current row's students
66 nextRowAvailableSeats &= ~(seatingMask >> 1); // No student to the upper-right
67 nextRowAvailableSeats &= ~(seatingMask << 1); // No student to the upper-left
68
69 // Recursively calculate maximum for remaining rows
70 maxStudentsCount = Math.max(
71 maxStudentsCount,
72 studentsInCurrentRow + dfs(nextRowAvailableSeats, currentRow + 1)
73 );
74 }
75 }
76
77 // Memoize and return result
78 return (memoTable[availableSeatMask][currentRow] = maxStudentsCount);
79 };
80
81 // Start DFS from first row with all available seats
82 return dfs(availableSeats[0], 0);
83}
84
Time and Space Complexity
Time Complexity: O(4^n × n × m)
The analysis breaks down as follows:
- For each row (total
m
rows), we iterate through all possible mask values from0
to2^n - 1
, which gives us2^n
iterations per recursive call - For each mask, we check if it's valid by verifying:
(seat | mask) != seat
: ensures students are only placed in available seats(mask & (mask << 1))
: ensures no two adjacent students in the same row
- The
bit_count()
operation takesO(n)
time in the worst case - Due to memoization with
@cache
, each unique state(seat, i)
is computed only once - The number of possible states for
seat
is at most2^n
(all possible configurations of available/blocked seats after considering previous row's impact) - There are
m
possible values fori
(row index) - Total states:
2^n × m
- For each state, we try
2^n
masks, and each mask validation and bit counting takesO(n)
- Therefore:
O(2^n × m × 2^n × n) = O(4^n × n × m)
Space Complexity: O(2^n × m)
The space usage comes from:
- The memoization cache stores results for each unique
(seat, i)
pair - There are at most
2^n
different values forseat
(representing different configurations of available seats after blocking positions based on previous row's students) - There are
m
different values fori
(row indices from 0 to m-1) - The list
ss
usesO(m)
space to store the seat configurations - The recursion depth is at most
m
- Total space:
O(2^n × m)
dominated by the cache storage
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Adjacency Check Logic
One of the most common mistakes is incorrectly checking whether students are sitting adjacent to each other. The condition (mask & (mask << 1))
only checks if a student has someone to their immediate left, but doesn't check for someone to their immediate right.
Incorrect approach:
# Only checks left adjacency if mask & (mask << 1): continue
Why it fails: Consider mask 0b110
(students in positions 1 and 2). The check 0b110 & 0b1100 = 0b100
is non-zero, correctly detecting adjacency. However, if we had mask 0b011
(students in positions 0 and 1), we need to ensure both left and right adjacencies are checked.
Correct solution:
The code actually handles this correctly because mask & (mask << 1)
effectively checks if any bit has a neighbor to its left, which is sufficient. When bit i
and bit i+1
are both set, shifting left by 1 and ANDing will produce a non-zero result.
2. Off-by-One Errors in Diagonal Blocking
When blocking diagonal seats for the next row, it's easy to mix up the shift directions or forget one of the diagonals.
Common mistake:
# Forgetting to block one diagonal or using wrong shift direction next_available &= ~(seating_mask << 1) # Only blocking one diagonal
Correct approach:
next_available &= ~(seating_mask << 1) # Block upper-left diagonal for next row next_available &= ~(seating_mask >> 1) # Block upper-right diagonal for next row
3. Inefficient State Space Exploration
Without memoization, the algorithm would repeatedly solve the same subproblems, leading to exponential time complexity.
Pitfall: Forgetting to use @cache
or implementing manual memoization incorrectly:
def dp(available_seats, row_index): # Missing @cache decorator
# ... recursive logic
Solution: Always use @cache
decorator or implement proper memoization:
@cache
def dp(available_seats, row_index):
# ... recursive logic
4. Incorrect Subset Check
When verifying if students are only placed in available seats, the subset check can be confusing.
Common mistake:
# Wrong subset check if seating_mask & available_seats != seating_mask: continue
Correct approach:
# Check if seating_mask is a subset of available_seats if (available_seats | seating_mask) != available_seats: continue
The correct check uses OR operation: if seating_mask
contains any bit that's not in available_seats
, the OR result will differ from available_seats
.
5. Edge Case: Empty Classroom
If all seats are broken or the classroom is empty, the algorithm should return 0. The current implementation handles this correctly, but it's important to verify:
Potential issue:
# If seats is empty or all seats are broken
if not seats or all(all(seat == '#' for seat in row) for row in seats):
return 0
The recursive approach naturally handles this because if no valid arrangements exist, the maximum will remain 0.
How does merge sort divide the problem into subproblems?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!