Select Cells in Grid With Maximum Score
HardPrompt
You are given a 2D matrix grid consisting of positive integers.
You have to select one or more cells from the matrix such that the following conditions are satisfied:
- No two selected cells are in the same row of the matrix.
- The values in the set of selected cells are unique.
Your score will be the sum of the values of the selected cells.
Return the maximum score you can achieve.
Example 1:
Input: grid = [[1,2,3],[4,3,2],[1,1,1]]
Output: 8
Explanation:

We can select the cells with values 1, 3, and 4 that are colored above.
Example 2:
Input: grid = [[8,7,6],[8,3,2]]
Output: 15
Explanation:

We can select the cells with values 7 and 8 that are colored above.
Constraints:
1 <= grid.length, grid[i].length <= 101 <= grid[i][j] <= 100
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a classic recursive backtracking technique. It explores every possible valid combination of cell selections by making a decision for each row: either to skip it or to pick one of its cells. The validity of a selection is checked by ensuring the chosen cell's value is unique among all previously selected cells.
Algorithm
- Define a recursive function
backtrack(rowIndex, currentScore, usedValues, grid). - The base case for the recursion is when
rowIndexequals the total number of rows. At this point, update the global maximum score withcurrentScore. - In the recursive step, for the current
rowIndex, explore two main branches:- Skip the row: Make a recursive call for the next row:
backtrack(rowIndex + 1, currentScore, usedValues, grid). - Select a cell: Iterate through each column
colof the current row.- Let
value = grid[rowIndex][col]. - If
valueis not present in theusedValuesset, it's a valid choice. - Add
valuetousedValues. - Make a recursive call for the next row with the updated score and used values:
backtrack(rowIndex + 1, currentScore + value, usedValues, grid). - After the recursive call returns, remove
valuefromusedValuesto backtrack and explore other possibilities.
- Let
- Skip the row: Make a recursive call for the next row:
- The initial call to start the process is
backtrack(0, 0, new HashSet<>(), grid).
Walkthrough
The brute-force backtracking approach systematically explores every valid combination of cells. The core of this method is a recursive function that traverses through the rows of the grid. For each row, it considers two possibilities: either skipping the row entirely or selecting one cell from it. If a cell is selected, its value must not have been chosen from any of the previous rows. This process builds a search tree where each path from the root to a leaf represents a valid selection. The score of each path is calculated, and the maximum score found across all paths is the answer.
class Solution { int maxScore = 0; public int selectCells(int[][] grid) { backtrack(0, 0, new HashSet<>(), grid); return maxScore; } private void backtrack(int rowIndex, int currentScore, Set<Integer> usedValues, int[][] grid) { if (rowIndex == grid.length) { maxScore = Math.max(maxScore, currentScore); return; } // Option 1: Skip the current row backtrack(rowIndex + 1, currentScore, usedValues, grid); // Option 2: Pick one cell from the current row for (int col = 0; col < grid[rowIndex].length; col++) { int value = grid[rowIndex][col]; if (!usedValues.contains(value)) { usedValues.add(value); backtrack(rowIndex + 1, currentScore + value, usedValues, grid); usedValues.remove(value); // backtrack } } }}Complexity
Time
O((n+1)^m), where `m` is the number of rows and `n` is the number of columns. For each of the `m` rows, we explore `n` options for selecting a cell plus one option for skipping the row. This leads to a recursion tree with approximately `(n+1)^m` nodes, making it infeasible for the given constraints.
Space
O(m), where `m` is the number of rows. This space is used for the recursion stack. The `usedValues` set also requires space, but its size is at most `m`.
Trade-offs
Pros
Conceptually simple and easy to implement.
Correctly explores the entire search space to guarantee the right answer if given enough time.
Cons
Extremely inefficient due to its exponential time complexity.
Will result in a 'Time Limit Exceeded' error for the given constraints.
A more efficient solution involves changing the perspective of the problem. Instead of making decisions row-by-row, we make decisions for each unique value present in the grid. By processing unique values from largest to smallest, we can decide whether to include a value in our sum and, if so, which available row to take it from. This structure allows for dynamic programming with memoization, drastically reducing redundant computations.
Algorithm
- Preprocessing:
- Create a map
locationsto store the row indices for each unique value in the grid. For example,locations.get(v)will return a list of rows containing the valuev. - Extract all unique values from the grid into a list,
uniqueValues. Sort this list in descending order. This helps in finding larger scores earlier, though it's not strictly necessary for correctness.
- Create a map
- DP with Memoization:
- Define a recursive function
solve(k, usedRowsMask)which returns the maximum score obtainable from considering valuesuniqueValues[k:], given that the rows indicated byusedRowsMaskare already occupied. kis the index of the current value being considered from theuniqueValueslist.usedRowsMaskis a bitmask where thei-th bit is 1 if a cell has already been selected from rowi.- Use a 2D array
memo[k][usedRowsMask]to store and retrieve results of subproblems.
- Define a recursive function
- Base Case: If
kequals the number of unique values, we have considered all values, so we return 0. - Recursive Step: For the current value
val = uniqueValues[k], calculate the maximum score by considering two choices:- Skip
val: The score issolve(k + 1, usedRowsMask). - Select
val: Iterate through all rowsrwherevalis located (from thelocationsmap). If rowris not yet used (i.e., ther-th bit inusedRowsMaskis 0), calculate the score asval + solve(k + 1, usedRowsMask | (1 << r)).
- Skip
- The function returns the maximum score found among all these choices, storing it in the memoization table before returning.
- The initial call is
solve(0, 0).
Walkthrough
This approach reframes the problem to make it suitable for dynamic programming. The state of our DP is defined by (k, usedRowsMask), where k is the index of the unique value we are currently considering, and usedRowsMask is a bitmask representing the rows that are already occupied. By iterating through the unique values and for each, deciding whether to use it (and in which available row) or skip it, we can build the solution optimally. Memoization is crucial to store the results of subproblems (k, usedRowsMask) to avoid re-calculating them.
class Solution { private List<Integer> uniqueValues; private Map<Integer, List<Integer>> locations; private int[][] memo; private int numRows; public int selectCells(int[][] grid) { numRows = grid.length; locations = new HashMap<>(); Set<Integer> valueSet = new HashSet<>(); for (int r = 0; r < numRows; r++) { for (int c = 0; c < grid[r].length; c++) { int val = grid[r][c]; valueSet.add(val); locations.computeIfAbsent(val, k -> new ArrayList<>()).add(r); } } uniqueValues = new ArrayList<>(valueSet); Collections.sort(uniqueValues, Collections.reverseOrder()); memo = new int[uniqueValues.size()][1 << numRows]; for (int[] row : memo) { Arrays.fill(row, -1); } return solve(0, 0); } private int solve(int k, int usedRowsMask) { if (k == uniqueValues.size()) { return 0; } if (memo[k][usedRowsMask] != -1) { return memo[k][usedRowsMask]; } // Option 1: Skip the current value int maxScore = solve(k + 1, usedRowsMask); // Option 2: Try to use the current value int currentValue = uniqueValues.get(k); if (locations.containsKey(currentValue)) { for (int row : locations.get(currentValue)) { // Check if the row is not used yet if ((usedRowsMask & (1 << row)) == 0) { // Use this value in this row maxScore = Math.max(maxScore, currentValue + solve(k + 1, usedRowsMask | (1 << row))); } } } return memo[k][usedRowsMask] = maxScore; }}Complexity
Time
O(m*n + U*log(U) + U * 2^m * m), where `m` is rows, `n` is columns, and `U` is the number of unique values. `O(m*n)` is for preprocessing the grid, `O(U*log(U))` for sorting unique values, and `O(U * 2^m * m)` for the DP calculation. Given `m <= 10` and `U <= 100`, this is very efficient.
Space
O(U * 2^m + U*m), where `U` is the number of unique values and `m` is the number of rows. The memoization table requires `O(U * 2^m)` space. The `locations` map can take up to `O(U*m)` space in the worst case.
Trade-offs
Pros
Highly efficient and solves the problem within the given time constraints.
Guarantees the optimal solution by using dynamic programming to avoid re-computation of overlapping subproblems.
Cons
More complex to conceptualize and implement compared to simple backtracking.
Requires additional space for the memoization table and preprocessing data structures.
Solutions
Solution
class Solution {public int maxScore(List<List<Integer>> grid) { int m = grid.size(); int mx = 0; boolean[][] g = new boolean[101][m + 1]; for (int i = 0; i < m; ++i) { for (int x : grid.get(i)) { g[x][i] = true; mx = Math.max(mx, x); } } int[][] f = new int[mx + 1][1 << m]; for (int i = 1; i <= mx; ++i) { for (int j = 0; j < 1 << m; ++j) { f[i][j] = f[i - 1][j]; for (int k = 0; k < m; ++k) { if (g[i][k] && (j >> k & 1) == 1) { f[i][j] = Math.max(f[i][j], f[i - 1][j ^ 1 << k] + i); } } } } return f[mx][(1 << m) - 1]; }}class Solution {public: int maxScore(vector<vector<int>> &grid) { int m = grid.size(); int mx = 0; bool g[101][11]{}; for (int i = 0; i < m; ++i) { for (int x : grid[i]) { g[x][i] = true; mx = max(mx, x); } } int f[mx + 1][1 << m]; memset(f, 0, sizeof(f)); for (int i = 1; i <= mx; ++i) { for (int j = 0; j < 1 << m; ++j) { f[i][j] = f[i - 1][j]; for (int k = 0; k < m; ++k) { if (g[i][k] && (j >> k & 1) == 1) { f[i][j] = max(f[i][j], f[i - 1][j ^ 1 << k] + i); } } } } return f[mx][(1 << m) - 1]; }};class Solution: def maxScore(self, grid: List[List[int]]) -> int: g = defaultdict(set) mx = 0 for i, row in enumerate(grid): for x in row: g[x]. add(i) mx = max(mx, x) m = len(grid) f = [[0] * (1 << m) for _ in range(mx + 1)] for i in range(1, mx + 1): for j in range(1 << m): f[i][j] = f[i - 1][j] for k in g[i]: if j >> k & 1: f[i][j] = max(f[i][j], f[i - 1][j ^ 1 << k] + i) return f[- 1][- 1]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.