We have an array and we need to split it into exactly k contiguous groups. Each group has a sum, and we want to minimize the maximum of those sums. Think of it like distributing work across k workers who must each handle a contiguous chunk, and we want to make the heaviest workload as light as possible.
The constraint that matters is that the subarrays must be contiguous. We cannot pick arbitrary elements for a group. We only choose where to place k - 1 dividers in the array. With n - 1 possible divider positions and k - 1 dividers to place, the number of ways to split grows combinatorially.
The goal is not to make all subarrays equal. It is to minimize the worst case, the single largest subarray sum. This minimax structure is what allows both a dynamic programming and a binary search solution.
1 <= nums.length <= 1000. With n up to 1000, the O(n^2 k) dynamic programming solution runs about 50 million operations in the worst case, which is tight but feasible. The O(n log(sum)) binary search is far faster.0 <= nums[i] <= 10^6. Elements can be zero, so some subarrays can have a sum of zero. The total sum reaches up to 10^9, which exceeds the 32-bit integer range, so the running sum and search bounds need a 64-bit type in languages like Java, C++, Go, C#, and Rust.1 <= k <= min(50, nums.length). k is small relative to n, which keeps the DP table dimension manageable.Try every possible way of splitting the array. We place k - 1 dividers among the n - 1 gaps between elements, and for each configuration we compute the sum of each subarray, take the maximum, and track the overall minimum.
Recursion expresses this directly. At each step we decide where the current subarray ends, compute its sum, and recurse on the remaining array with one fewer split to make. The base case is when one split remains, where the entire remaining array forms the last subarray.
A prefix sum array lets us compute any subarray sum in O(1), which avoids re-adding elements on every recursive call.
solve(index, splitsLeft) that returns the minimum possible largest sum when splitting nums[index..n-1] into splitsLeft subarrays.splitsLeft == 1, return the sum of nums[index..n-1].index to n - splitsLeft), compute the sum of the current subarray, recursively solve the rest, and take the maximum of the two. Track the minimum across all choices.solve(0, k).Loading animation...
The recursion recomputes the same subproblems repeatedly. The state solve(index, splitsLeft) depends only on those two values, yet it can be reached from many different earlier split choices. Caching each (index, splitsLeft) pair collapses the work to one computation per state, which is the basis for the dynamic programming approach.
There are only n * k distinct states, one for each pair of array position and remaining split count, so caching turns the exponential recursion into a polynomial table fill.
Building the table bottom-up, let dp[j][i] represent the minimum possible largest sum when splitting the first i elements into j subarrays. To fill in dp[j][i], we try every possible position p for the start of the last subarray. The last subarray covers nums[p..i-1], and the first j-1 subarrays cover nums[0..p-1]. So dp[j][i] = min over all valid p of max(dp[j-1][p], sum(p..i-1)).
The base case is dp[1][i] = sum of the first i elements, because with one subarray, we must take everything.
The recurrence is correct because the largest sum of a full split is the larger of two independent parts: the largest sum among the first j-1 subarrays (already minimized in dp[j-1][p]) and the sum of the last subarray (prefix[i] - prefix[p]). These two parts share no elements, so their costs combine through max and the optimal choice for the left part is unaffected by where the last subarray sits. Minimizing over every start position p of the last subarray therefore gives the true optimum for dp[j][i].
(k+1) x (n+1), initialized to infinity.dp[1][i] = prefix[i] for all i from 1 to n.j from 2 to k:i from j to n (need at least j elements for j splits):p from j-1 to i-1:lastSum = prefix[i] - prefix[p]dp[j][i] = min(dp[j][i], max(dp[j-1][p], lastSum))dp[k][n].Loading animation...
The DP enumerates every split position. The answer itself is a single number within a bounded range, and the next approach searches that range directly, checking feasibility instead of constructing the split.
Instead of asking "what is the best way to split?", flip the question to "given a maximum allowed subarray sum of X, can we split the array into k or fewer subarrays?"
That feasibility question has a greedy answer. Scan left to right, extending the current subarray as far as possible. When adding the next element would push the running sum past X, start a new subarray. Count the subarrays needed. If the count is at most k, then X is achievable.
The feasibility function is monotonic. If a max sum of X permits a valid split, then X + 1 permits the same split, so it is also feasible. If X does not permit a valid split, neither does X - 1. Feasibility flips from false to true at a single threshold, and that threshold is the answer, so binary search on X finds it.
The search range is [max(nums), sum(nums)]. The lower bound is max(nums), because the largest single element must fit inside some subarray no matter how the array is split. The upper bound is sum(nums), the value when k = 1 and one subarray holds everything.
Packing as much as possible into the current subarray before starting a new one uses the fewest subarrays for a given limit X. Suppose some split used fewer subarrays than the greedy count. Walk both splits from the left: the greedy first cut sits at or after any other valid first cut, since greedy only cuts when forced. By the same argument at every step, the greedy boundaries never fall behind, so greedy cannot use more cuts than any valid split. If even the greedy count exceeds k, no split fits within k subarrays, so the check correctly reports X as infeasible.
left = max(nums) and right = sum(nums).left < right:mid = left + (right - left) / 2k subarrays where each has sum at most mid.right = mid.left = mid + 1.canSplit): scan left to right, maintaining a running sum. Whenever adding the next element would exceed mid, start a new subarray. Count the subarrays needed.left.Loading animation...