Maximum Sum With at Most K Elements
MedPrompt
You are given a 2D integer matrix grid of size n x m, an integer array limits of length n, and an integer k. The task is to find the maximum sum of at most k elements from the matrix grid such that:
-
The number of elements taken from the
ithrow ofgriddoes not exceedlimits[i].
Return the maximum sum.
Example 1:
Input: grid = [[1,2],[3,4]], limits = [1,2], k = 2
Output: 7
Explanation:
- From the second row, we can take at most 2 elements. The elements taken are 4 and 3.
- The maximum possible sum of at most 2 selected elements is
4 + 3 = 7.
Example 2:
Input: grid = [[5,3,7],[8,2,6]], limits = [2,2], k = 3
Output: 21
Explanation:
- From the first row, we can take at most 2 elements. The element taken is 7.
- From the second row, we can take at most 2 elements. The elements taken are 8 and 6.
- The maximum possible sum of at most 3 selected elements is
7 + 8 + 6 = 21.
Constraints:
n == grid.length == limits.lengthm == grid[i].length1 <= n, m <= 5000 <= grid[i][j] <= 1050 <= limits[i] <= m0 <= k <= min(n * m, sum(limits))
Approaches
3 approaches with complexity analysis and trade-offs.
This problem can be modeled as a variation of the Multiple-Choice Knapsack problem. For each row (item type), we can choose to take c elements (items), where c is between 0 and limits[i]. The 'weight' of taking c elements is c, and the 'value' is the sum of the c largest elements in that row. The total 'weight' (number of elements) cannot exceed k. We want to maximize the total 'value' (sum).
A standard dynamic programming approach can solve this. We define dp[j] as the maximum sum we can obtain by choosing a total of j elements from the rows considered so far. We iterate through each row and update the dp array based on the choices available for that row.
Algorithm
- Preprocessing:
- For each row
iin thegrid, sort it in descending order. - For each row
i, compute its prefix sums. LetP[i][j]be the sum of thejlargest elements in rowi.P[i][0]is 0.
- For each row
- Dynamic Programming Setup:
- Create a 1D DP array,
dp, of sizek + 1, initialized to zeros.dp[j]will store the maximum sum achievable using exactlyjelements from the rows processed so far.
- Create a 1D DP array,
- DP Iteration:
- Iterate through each row
ifrom0ton-1. - For each row, update the
dparray. Iteratejfromkdown to1. - For each
j, iterate through the number of elementscto take from the current rowi, where1 <= c <= limits[i]andc <= j. - The transition is:
dp[j] = max(dp[j], dp[j - c] + P[i][c]).
- Iterate through each row
- Result:
- Since we can take at most
kelements and all element values are non-negative, the maximum sum will be the largest value in thedparray after processing all rows. The answer ismax(dp[0], dp[1], ..., dp[k]).
- Since we can take at most
Walkthrough
First, we preprocess the data to make the DP transitions efficient. For each row, we sort it in descending order to easily access the largest elements. Then, we compute the prefix sums for each sorted row. prefixSums[i][c] will store the sum of the c largest elements of row i.
The DP state dp[j] represents the maximum sum using exactly j elements. We use a 1D array for space optimization. We iterate through each row i and for each row, we update the dp array. We iterate the total elements j from k down to 1 (to avoid using elements from the same row multiple times in a single update step). For each j, we consider taking c elements from the current row i, where c ranges from 1 to limits[i]. The new dp[j] will be the maximum of its current value and dp[j-c] + prefixSums[i][c], which represents the sum from previous rows plus the sum from taking c elements from the current row.
After iterating through all the rows, the dp array holds the maximum sums for taking exactly 0, 1, ..., k elements. The final answer is the maximum value in this dp array.
class Solution { public long maximumSum(int[][] grid, int[] limits, int k) { int n = grid.length; int m = grid[0].length; // 1. Preprocessing: Sort rows and compute prefix sums long[][] prefixSums = new long[n][m + 1]; for (int i = 0; i < n; i++) { Arrays.sort(grid[i]); // Reverse for descending order for (int l = 0, r = m - 1; l < r; l++, r--) { int temp = grid[i][l]; grid[i][l] = grid[i][r]; grid[i][r] = temp; } prefixSums[i][0] = 0; for (int j = 0; j < m; j++) { prefixSums[i][j + 1] = prefixSums[i][j] + grid[i][j]; } } // 2. DP with space optimization long[] dp = new long[k + 1]; for (int i = 0; i < n; i++) { for (int j = k; j >= 1; j--) { for (int c = 1; c <= Math.min(j, limits[i]); c++) { if (j - c >= 0) { dp[j] = Math.max(dp[j], dp[j - c] + prefixSums[i][c]); } } } } // 3. Result is the max value in the dp array long maxSum = 0; for (long sum : dp) { maxSum = Math.max(maxSum, sum); } return maxSum; }}Complexity
Time
O(n*m*log(m) + n*k*m). `O(n*m*log(m))` for sorting all rows. The DP calculation involves three nested loops, iterating up to `n`, `k`, and `m` times respectively, leading to `O(n*k*m)`.
Space
O(n*m + k). `O(n*m)` for storing the prefix sums and `O(k)` for the DP array.
Trade-offs
Pros
It's a standard and relatively intuitive approach for knapsack-like problems.
The logic is guaranteed to find the optimal solution.
Cons
The time complexity of
O(n * k * m)is too high for the given constraints, especially whenkandmare large. This will likely result in a 'Time Limit Exceeded' error.
A more efficient method is a greedy approach. Since we want to maximize the sum, it's always optimal to pick the largest available element at each step. We can maintain the 'largest available element' from each row and pick the best among them.
This can be implemented efficiently using a max-heap. The heap will store the next-best candidate element from each row. At each step, we extract the overall best element from the heap, add it to our sum, and then insert the next-best element from the same row back into the heap, provided we don't violate that row's specific limit.
Algorithm
- Preprocessing:
- Sort each row of the
gridin descending order. This allows us to access the largest elements of each row sequentially.
- Sort each row of the
- Setup Max-Heap:
- Create a max-heap (Priority Queue) to store entries of
{value, rowIndex}. - Initialize the heap by adding the largest element from each row
i(i.e.,grid[i][0]), providedlimits[i] > 0and the row is not empty.
- Create a max-heap (Priority Queue) to store entries of
- Greedy Selection:
- Initialize
totalSum = 0and an arrayrowIndicesto track the index of the next element to consider from each row. - Loop up to
ktimes or until the heap is empty:- Extract the element with the maximum value from the heap. Let this be
{value, row}. - Add
valuetototalSum. - Increment the index for the
row(rowIndices[row]++). - If there are more elements to consider in that
row(i.e.,rowIndices[row] < m) and the row's limit has not been reached (rowIndices[row] < limits[row]), add the next largest element from that row (grid[row][rowIndices[row]]) to the heap.
- Extract the element with the maximum value from the heap. Let this be
- Initialize
- Result:
- Return
totalSum.
- Return
Walkthrough
The core idea is that to get the maximum sum, we should always pick the largest available number. The constraints are that we can pick at most k numbers in total, and from each row i, at most limits[i] numbers.
First, we sort each row in descending order. This makes it easy to find the 1st, 2nd, 3rd, etc., largest elements of any row.
We use a max-heap to keep track of the current best candidate from each row. Initially, we populate the heap with the largest element of each row. Then, we repeat k times: extract the max element from the heap. Suppose this element came from row r. We add its value to our total sum. Then, we offer the next largest element from row r to the heap, but only if we haven't exhausted the elements in row r or exceeded the limits[r] for that row. This process ensures that at every step, we are picking the globally largest element available that satisfies all constraints.
This greedy strategy is optimal because of an exchange argument: if there were a better solution, it must differ from the greedy one at some step. At the first differing step, the greedy algorithm chose an element g while the optimal solution chose o. By the greedy choice property, g >= o. We can swap o with g in the optimal solution to get an equally good or better solution, moving it closer to the greedy solution. Repeating this proves the greedy solution is optimal.
class Solution { public long maximumSum(int[][] grid, int[] limits, int k) { int n = grid.length; int m = grid[0].length; // 1. Sort each row in descending order for (int i = 0; i < n; i++) { Arrays.sort(grid[i]); for (int l = 0, r = m - 1; l < r; l++, r--) { int temp = grid[i][l]; grid[i][l] = grid[i][r]; grid[i][r] = temp; } } // Max-heap to store {value, row_index} PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[0] - a[0]); // 2. Initialize heap with the largest element from each row for (int i = 0; i < n; i++) { if (limits[i] > 0 && m > 0) { pq.offer(new int[]{grid[i][0], i}); } } long totalSum = 0; int[] rowIndices = new int[n]; // Tracks elements taken from each row // 3. Greedily pick up to k largest elements for (int i = 0; i < k && !pq.isEmpty(); i++) { int[] top = pq.poll(); int value = top[0]; int row = top[1]; totalSum += value; rowIndices[row]++; // If there are more elements in the row and we haven't hit the limit, // offer the next element from that row to the heap. if (rowIndices[row] < m && rowIndices[row] < limits[row]) { pq.offer(new int[]{grid[row][rowIndices[row]], row}); } } return totalSum; }}Complexity
Time
O(n*m*log(m) + k*log(n)). `O(n*m*log(m))` for sorting all rows. The main loop runs up to `k` times, with each heap operation (pop and push) taking `O(log(n))` time.
Space
O(n*m). `O(n*m)` to store the grid (if modification is not allowed, otherwise can be `O(1)` for sorting in-place). The heap requires `O(n)` space.
Trade-offs
Pros
Significantly more efficient than the DP approach.
Guaranteed to be optimal.
The logic is intuitive and reflects the nature of the problem (maximizing a sum).
Cons
The time complexity is dependent on
k. Ifkis very large (close ton*m), this approach can be slower than the binary search method.
The most efficient approach uses binary search on the answer. Instead of picking elements one by one, we can determine a threshold value T and decide to pick all elements greater than or equal to T. The number of elements we can pick is a monotonic function of T: the lower the threshold T, the more elements we can pick.
This monotonicity allows us to binary search for the optimal threshold T, which represents the value of the smallest element in our chosen set of k elements. After finding this threshold, we can calculate the total sum by summing up all elements greater than T and then adding T for the remaining slots up to k.
Algorithm
- Preprocessing:
- Sort each row of the
gridin descending order:O(n*m*log m). - Compute prefix sums for each sorted row to quickly find the sum of the top
xelements:O(n*m).
- Sort each row of the
- Binary Search for Threshold:
- The key insight is that the number of elements we can pick is monotonic with respect to a minimum value threshold. If we can pick
Celements with value>= T, we can also pick at leastCelements with value>= T-1. - We can binary search for the optimal threshold value
T. ThisTwill be the value of thek-th element we pick (or 0 if we pick fewer thankelements). - The search space for
Tis[0, 10^5 + 1]. - For each
midvalue in the binary search, we have acheck(mid)function that counts how many elements with value>= midcan be picked across all rows while respecting thelimitsarray. This check can be done inO(n*log m)by using binary search on each sorted row. - We want to find the largest
Tsuch thatcheck(T) >= k.
- The key insight is that the number of elements we can pick is monotonic with respect to a minimum value threshold. If we can pick
- Calculate Final Sum:
- Once the optimal threshold
Tis found, we can calculate the final sum. - First, sum up all elements that are strictly greater than
T, respecting each row's limit. This can be done inO(n*log m)using the precomputed prefix sums. - Let the count of these elements be
elementsCount. - We need to pick
k - elementsCountmore elements. SinceTis our threshold, all these remaining elements will have the valueT. - The final sum is
(sum of elements > T) + (k - elementsCount) * T.
- Once the optimal threshold
- Result:
- Return the calculated total sum.
Walkthrough
This approach hinges on binary searching for the value of the k-th largest element we select. Let's call this threshold T.
First, we preprocess by sorting each row in descending order and calculating their prefix sums. This allows for efficient queries later.
We then perform a binary search on the possible values of elements, from 0 to 100001. For a given value mid in our search, we define a function check(mid) that determines if it's possible to select at least k elements from the grid with values >= mid, while respecting the limits for each row. check(mid) works by iterating through each row, finding how many elements are >= mid (using binary search on the sorted row), taking the minimum of that count and limits[i], and summing these counts up. If the total count is >= k, check(mid) returns true.
The main binary search aims to find the largest value T for which check(T) is true. This T is the minimum value an element can have to be included in our top k choices.
To calculate the final sum, we first take all elements strictly greater than T. For each row, we find how many elements are > T, take the minimum of this count and limits[i], and use our prefix sum array to add their sum to our totalSum. We also count how many elements we've taken so far (elementsCount). Finally, we need to take k - elementsCount more elements. By the definition of our threshold T, all these must have the value T. So, we add (k - elementsCount) * T to our totalSum.
class Solution { public long maximumSum(int[][] grid, int[] limits, int k) { int n = grid.length; int m = grid[0].length; // 1. Preprocessing for (int i = 0; i < n; i++) { Arrays.sort(grid[i]); for (int l = 0, r = m - 1; l < r; l++, r--) { int temp = grid[i][l]; grid[i][l] = grid[i][r]; grid[i][r] = temp; } } long[][] prefixSums = new long[n][m + 1]; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { prefixSums[i][j + 1] = prefixSums[i][j] + grid[i][j]; } } // 2. Binary search for the threshold value T int low = 0, high = 100001, T = 0; while (low <= high) { int mid = low + (high - low) / 2; if (canTakeAtLeastK(grid, limits, k, mid)) { T = mid; low = mid + 1; } else { high = mid - 1; } } // 3. Calculate final sum long totalSum = 0; int elementsCount = 0; for (int i = 0; i < n; i++) { int countGreaterThanT = countGreater(grid[i], T); int numToTake = Math.min(countGreaterThanT, limits[i]); totalSum += prefixSums[i][numToTake]; elementsCount += numToTake; } if (elementsCount < k) { totalSum += (long)(k - elementsCount) * T; } return totalSum; } private boolean canTakeAtLeastK(int[][] grid, int[] limits, int k, int threshold) { long count = 0; for (int i = 0; i < grid.length; i++) { count += Math.min(countGreaterOrEqual(grid[i], threshold), limits[i]); if (count >= k) return true; } return false; } private int countGreaterOrEqual(int[] row, int val) { int l = 0, r = row.length - 1, ans = row.length; while (l <= r) { int mid = l + (r - l) / 2; if (row[mid] < val) { ans = mid; r = mid - 1; } else { // row[mid] >= val l = mid + 1; } } return ans; } private int countGreater(int[] row, int val) { int l = 0, r = row.length - 1, ans = row.length; while (l <= r) { int mid = l + (r - l) / 2; if (row[mid] <= val) { ans = mid; r = mid - 1; } else { // row[mid] > val l = mid + 1; } } return ans; }}Complexity
Time
O(n*m*log(m) + n*log(m)*log(V)). The dominant part is sorting, `O(n*m*log(m))`. The binary search runs `log(V)` times (where `V` is the max value of an element), and each check takes `O(n*log(m))`. The final calculation also takes `O(n*log(m))`. This is effectively dominated by the initial sort.
Space
O(n*m). `O(n*m)` is needed for storing the sorted grid and the prefix sums.
Trade-offs
Pros
This is the most efficient approach, with a time complexity that does not depend on
k.It performs very well, especially for large values of
k.
Cons
The logic is more complex to understand and implement correctly compared to the greedy approach.
Requires careful handling of edge cases in the binary search and final sum calculation.
Solutions
Solution
class Solution {public long maxSum(int[][] grid, int[] limits, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); int n = grid.length; for (int i = 0; i < n; ++i) { int[] nums = grid[i]; int limit = limits[i]; Arrays.sort(nums); for (int j = 0; j < limit; ++j) { pq.offer(nums[nums.length - j - 1]); if (pq.size() > k) { pq.poll(); } } } long ans = 0; for (int x : pq) { ans += x; } return ans; }}class Solution {public: long long maxSum(vector<vector<int>> &grid, vector<int> &limits, int k) { priority_queue<int, vector<int>, greater<int>> pq; int n = grid.size(); for (int i = 0; i < n; ++i) { vector<int> nums = grid[i]; int limit = limits[i]; ranges ::sort(nums); for (int j = 0; j < limit; ++j) { pq.push(nums[nums.size() - j - 1]); if (pq.size() > k) { pq.pop(); } } } long long ans = 0; while (!pq.empty()) { ans += pq.top(); pq.pop(); } return ans; }};class Solution: def maxSum(self, grid: List[List[int]], limits: List[int], k: int) -> int: pq = [] for nums, limit in zip(grid, limits): nums . sort() for _ in range(limit): heappush(pq, nums . pop()) if len(pq) > k: heappop(pq) return sum(pq)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.