LeetCode 1947 - Maximum Compatibility Score Sum
The problem is asking us to optimally pair students with mentors such that the sum of their compatibility scores is maximized. Each student and each mentor has answered n yes/no questions represented by 0s and 1s.
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Backtracking, Bit Manipulation, Bitmask
Solution
Problem Understanding
The problem is asking us to optimally pair students with mentors such that the sum of their compatibility scores is maximized. Each student and each mentor has answered n yes/no questions represented by 0s and 1s. A student-mentor pair's compatibility score is the count of matching answers. For instance, if a student answers [1, 0, 1] and the mentor answers [0, 0, 1], the score is 2 because two answers match.
The input consists of two m x n matrices: students and mentors. Each row of students represents one student's answers, and each row of mentors represents one mentor's answers. The output is a single integer representing the maximum sum of compatibility scores achievable by assigning each student to exactly one mentor and vice versa.
The constraints are small: 1 <= m, n <= 8. This allows us to consider solutions that involve examining all permutations of student-mentor assignments since 8! = 40320 is computationally feasible. We also know that each answer is binary, which allows for bit manipulation to speed up score calculations.
Important edge cases include: all students and mentors having identical answers (maximum score), all answers completely opposite (score of zero), and the smallest case where m = 1 and n = 1.
Approaches
The brute-force approach is to generate all permutations of mentor indices and assign each permutation to students. For each assignment, we calculate the total compatibility score by summing the scores for each student-mentor pair. This guarantees correctness because it exhaustively examines every possible assignment. However, its time complexity is O(m! * m * n) due to m! permutations and calculating compatibility for each student-mentor pair, which is inefficient for larger m.
The key insight for an optimal solution is to use backtracking with pruning. Instead of generating all permutations at once, we recursively assign students to mentors, marking mentors as assigned. For each partial assignment, we track the accumulated score. If a branch cannot beat the current maximum score (for example, if all remaining possible matches cannot improve the score), we prune that branch. This reduces redundant computation while guaranteeing the optimal solution. Bitmasking can also be used to efficiently track which mentors are assigned.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(m! * m * n) | O(m) | Generate all permutations of mentor assignments and calculate scores |
| Backtracking with Bitmask | O(m * 2^m * n) | O(2^m) | Use recursion with a bitmask to track assigned mentors and prune suboptimal paths |
Algorithm Walkthrough
- Precompute compatibility scores for all student-mentor pairs in a 2D array
score[i][j], whereiis the student index andjis the mentor index. This allows constant-time lookup during backtracking. - Define a recursive function
dfs(student_index, mask)that tries to assign the current student to any unassigned mentor.maskis a bitmask where thej-th bit is1if mentorjis assigned. - If
student_indexequalsm, return0since all students are assigned. - For each mentor
jfrom0tom-1, check ifmask & (1 << j)is0(mentorjis unassigned). If yes, recursively calldfs(student_index + 1, mask | (1 << j))and addscore[student_index][j]to the result. - Track the maximum sum across all recursive calls for the current student.
- Return the maximum sum obtained from assigning all students.
Why it works: The backtracking explores all valid student-mentor assignments while pruning invalid states using the bitmask. Precomputing scores allows constant-time evaluation for each assignment. Since all permutations are considered, the algorithm guarantees the maximum compatibility sum is found.
Python Solution
from typing import List
class Solution:
def maxCompatibilitySum(self, students: List[List[int]], mentors: List[List[int]]) -> int:
m, n = len(students), len(students[0])
# Precompute compatibility scores
score = [[0] * m for _ in range(m)]
for i in range(m):
for j in range(m):
score[i][j] = sum(students[i][k] == mentors[j][k] for k in range(n))
memo = {}
def dfs(student_index: int, mask: int) -> int:
if student_index == m:
return 0
if (student_index, mask) in memo:
return memo[(student_index, mask)]
max_sum = 0
for j in range(m):
if not mask & (1 << j):
current_sum = score[student_index][j] + dfs(student_index + 1, mask | (1 << j))
max_sum = max(max_sum, current_sum)
memo[(student_index, mask)] = max_sum
return max_sum
return dfs(0, 0)
The Python implementation first precomputes the compatibility scores in score[i][j]. The dfs function uses recursion and a bitmask to efficiently track which mentors are assigned. Memoization ensures that repeated subproblems are not recomputed, reducing the effective time complexity from exponential to O(m * 2^m).
Go Solution
func maxCompatibilitySum(students [][]int, mentors [][]int) int {
m, n := len(students), len(students[0])
// Precompute compatibility scores
score := make([][]int, m)
for i := 0; i < m; i++ {
score[i] = make([]int, m)
for j := 0; j < m; j++ {
count := 0
for k := 0; k < n; k++ {
if students[i][k] == mentors[j][k] {
count++
}
}
score[i][j] = count
}
}
memo := make(map[[2]int]int)
var dfs func(int, int) int
dfs = func(studentIndex int, mask int) int {
if studentIndex == m {
return 0
}
key := [2]int{studentIndex, mask}
if val, ok := memo[key]; ok {
return val
}
maxSum := 0
for j := 0; j < m; j++ {
if mask&(1<<j) == 0 {
currentSum := score[studentIndex][j] + dfs(studentIndex+1, mask|(1<<j))
if currentSum > maxSum {
maxSum = currentSum
}
}
}
memo[key] = maxSum
return maxSum
}
return dfs(0, 0)
}
In Go, the approach is nearly identical. The main differences are Go’s type system, using slices instead of lists, and using a [2]int array as the memoization key instead of a tuple. Integer bit operations work the same as in Python.
Worked Examples
Example 1
students = [[1,1,0],[1,0,1],[0,0,1]]
mentors = [[1,0,0],[0,0,1],[1,1,0]]
Compatibility scores:
| Student \ Mentor | 0 | 1 | 2 |
|---|---|---|---|
| 0 | 2 | 1 | 3 |
| 1 | 2 | 2 | 1 |
| 2 | 0 | 3 | 1 |
Backtracking assignment steps:
- Start with student 0: assign mentor 2 → score 3
- Student 1: assign mentor 0 → score 2
- Student 2: assign mentor 1 → score 3
Total = 3 + 2 + 3 = 8
Example 2
All students [0,0], all mentors [1,1]. Every score is 0. Any assignment yields a total score of 0.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m * 2^m * n) | Each student has up to 2^m mask states; for each, we check m mentors and compute scores in O(1) using precomputation |
| Space | O(2^m + m^2) | 2^m for memoization and m^2 for precomputed score table |
The backtracking with memoization reduces the factorial complexity of brute-force permutations to exponential but manageable due to small m.
Test Cases
# Provided examples
assert Solution().maxCompatibilitySum([[1,1,0],[1,0,1],[0,0,1]], [[1,0,0],[0,0,1],[1,1,0]]) == 8 # mixed case
assert Solution().maxCompatibilitySum([[0,0],[0,0],[0,0]], [[1,1],[1,1],[1,1]]) == 0 # no matches
# Edge cases
assert Solution().maxCompatibilitySum([[1]], [[1]]) == 1 # smallest