223 lines
10 KiB
YAML
223 lines
10 KiB
YAML
title: Split Array Largest Sum
|
||
slug: split-array-largest-sum
|
||
difficulty: hard
|
||
leetcode_id: 410
|
||
leetcode_url: https://leetcode.com/problems/split-array-largest-sum/
|
||
categories:
|
||
- arrays
|
||
- binary-search
|
||
- dynamic-programming
|
||
patterns:
|
||
- slug: binary-search
|
||
is_optimal: true
|
||
- slug: greedy
|
||
is_optimal: false
|
||
|
||
function_signature: "def split_array(nums: list[int], k: int) -> int:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { nums: [7, 2, 5, 10, 8], k: 2 }
|
||
expected: 18
|
||
- input: { nums: [1, 2, 3, 4, 5], k: 2 }
|
||
expected: 9
|
||
hidden:
|
||
- input: { nums: [1, 4, 4], k: 3 }
|
||
expected: 4
|
||
- input: { nums: [10, 5, 13, 4, 8, 4, 5, 11, 14, 9, 16, 10, 20, 8], k: 8 }
|
||
expected: 25
|
||
- input: { nums: [1, 2, 3, 4, 5], k: 1 }
|
||
expected: 15
|
||
- input: { nums: [1, 2, 3, 4, 5], k: 5 }
|
||
expected: 5
|
||
- input: { nums: [2, 3, 1, 2, 4, 3], k: 5 }
|
||
expected: 4
|
||
|
||
description: |
|
||
Given an integer array `nums` and an integer `k`, split `nums` into `k` non-empty subarrays such that the largest sum of any subarray is **minimized**.
|
||
|
||
Return *the minimized largest sum of the split*.
|
||
|
||
A **subarray** is a contiguous part of the array.
|
||
|
||
constraints: |
|
||
- `1 <= nums.length <= 1000`
|
||
- `0 <= nums[i] <= 10^6`
|
||
- `1 <= k <= min(50, nums.length)`
|
||
|
||
examples:
|
||
- input: "nums = [7,2,5,10,8], k = 2"
|
||
output: "18"
|
||
explanation: "There are four ways to split nums into two subarrays. The best way is to split it into [7,2,5] and [10,8], where the largest sum among the two subarrays is only 18."
|
||
- input: "nums = [1,2,3,4,5], k = 2"
|
||
output: "9"
|
||
explanation: "There are four ways to split nums into two subarrays. The best way is to split it into [1,2,3] and [4,5], where the largest sum among the two subarrays is only 9."
|
||
|
||
explanation:
|
||
intuition: |
|
||
This problem asks us to split an array into `k` parts to minimize the maximum subarray sum. At first glance, it seems like we need to try all possible ways to partition the array — but that's exponentially complex.
|
||
|
||
Here's the key insight: **instead of searching for where to split, search for what the answer could be**.
|
||
|
||
Think of it like this: imagine you're a manager assigning work to `k` workers. Each worker must handle a contiguous segment of tasks, and you want to minimize the maximum workload any single worker receives. You could ask: "Is it possible to distribute the work so that no worker handles more than X units?"
|
||
|
||
If you can answer that question efficiently, you can use **binary search** to find the smallest valid X. The answer lies somewhere between:
|
||
- **Lower bound**: the largest single element (one subarray must contain at least the max element)
|
||
- **Upper bound**: the sum of all elements (one subarray contains everything)
|
||
|
||
For any candidate answer `mid`, we greedily check: can we split the array into at most `k` subarrays where each has sum ≤ `mid`? If yes, we might be able to do better (search lower). If no, we need a larger limit (search higher).
|
||
|
||
approach: |
|
||
We solve this using **Binary Search on the Answer**:
|
||
|
||
**Step 1: Define the search space**
|
||
|
||
- `left`: Set to `max(nums)` — the answer can't be smaller than the largest element
|
||
- `right`: Set to `sum(nums)` — the answer can't exceed putting everything in one subarray
|
||
|
||
|
||
|
||
**Step 2: Binary search for the minimum valid answer**
|
||
|
||
- Calculate `mid = (left + right) // 2`
|
||
- Check if we can split the array into at most `k` subarrays with each sum ≤ `mid`
|
||
- If yes: this `mid` works, but maybe we can do better — set `right = mid`
|
||
- If no: we need a larger limit — set `left = mid + 1`
|
||
|
||
|
||
|
||
**Step 3: Greedy feasibility check (can_split function)**
|
||
|
||
- Iterate through the array, accumulating a running sum
|
||
- When adding the next element would exceed `mid`, start a new subarray
|
||
- Count how many subarrays we need
|
||
- Return `True` if we need ≤ `k` subarrays
|
||
|
||
|
||
|
||
**Step 4: Return the result**
|
||
|
||
- When `left == right`, we've found the minimum valid maximum subarray sum
|
||
- Return `left`
|
||
|
||
common_pitfalls:
|
||
- title: Trying All Partitions (Exponential Blowup)
|
||
description: |
|
||
A natural first thought is to enumerate all ways to split the array into `k` parts. For an array of length `n`, there are `C(n-1, k-1)` ways to place `k-1` dividers among `n-1` gaps.
|
||
|
||
With `n = 1000` and `k = 50`, this is astronomically large — far too many combinations to check. This approach won't pass time limits.
|
||
wrong_approach: "Enumerate all partition combinations"
|
||
correct_approach: "Binary search on the answer with greedy validation"
|
||
|
||
- title: Wrong Search Space Bounds
|
||
description: |
|
||
Setting `left = 0` is incorrect because the answer must be at least `max(nums)` — a subarray containing only the largest element has that sum.
|
||
|
||
For example, with `nums = [10, 1, 1, 1]` and `k = 4`, the answer is `10` (each element in its own subarray), not `4`.
|
||
wrong_approach: "left = 0, right = sum(nums)"
|
||
correct_approach: "left = max(nums), right = sum(nums)"
|
||
|
||
- title: Off-by-One in Subarray Counting
|
||
description: |
|
||
When checking feasibility, remember that you start with one subarray. Each time you "cut" to start a new subarray, increment the count.
|
||
|
||
A common bug is initializing `count = 0` instead of `count = 1`, which underestimates the number of subarrays needed.
|
||
wrong_approach: "Initialize subarray count to 0"
|
||
correct_approach: "Initialize subarray count to 1 (first subarray)"
|
||
|
||
- title: Using Exclusive Upper Bound Incorrectly
|
||
description: |
|
||
This is a "minimize the maximum" binary search. When `can_split(mid)` returns `True`, set `right = mid` (not `mid - 1`) because `mid` itself might be the answer.
|
||
|
||
Using `right = mid - 1` could skip the optimal answer.
|
||
wrong_approach: "right = mid - 1 when feasible"
|
||
correct_approach: "right = mid when feasible (mid might be optimal)"
|
||
|
||
key_takeaways:
|
||
- "**Binary search on the answer**: When searching for an optimal value in a range, binary search the answer space and validate with a greedy check"
|
||
- "**Greedy feasibility**: The `can_split` function greedily packs elements into subarrays — this works because we're checking a fixed limit, not optimizing"
|
||
- "**Minimize-the-maximum pattern**: This problem structure (minimize the max of subarray sums) appears in many variants: allocating pages to students, shipping packages within D days, etc."
|
||
- "**Search space bounds matter**: The lower bound is `max(nums)`, not `0` — understand why the bounds are what they are"
|
||
|
||
time_complexity: "O(n × log(sum(nums) - max(nums))). We perform binary search over a range of size `sum - max`, and each feasibility check takes O(n) time."
|
||
space_complexity: "O(1). We only use a constant number of variables for tracking bounds and subarray sums."
|
||
|
||
solutions:
|
||
- approach_name: Binary Search on Answer
|
||
is_optimal: true
|
||
code: |
|
||
def splitArray(nums: list[int], k: int) -> int:
|
||
def can_split(max_sum: int) -> bool:
|
||
"""Check if we can split into <= k subarrays with each sum <= max_sum."""
|
||
subarrays = 1 # Start with one subarray
|
||
current_sum = 0
|
||
|
||
for num in nums:
|
||
# Would adding this element exceed our limit?
|
||
if current_sum + num > max_sum:
|
||
# Start a new subarray with this element
|
||
subarrays += 1
|
||
current_sum = num
|
||
else:
|
||
# Add to current subarray
|
||
current_sum += num
|
||
|
||
return subarrays <= k
|
||
|
||
# Search space: [max element, total sum]
|
||
left = max(nums)
|
||
right = sum(nums)
|
||
|
||
# Binary search for minimum valid maximum
|
||
while left < right:
|
||
mid = (left + right) // 2
|
||
|
||
if can_split(mid):
|
||
# mid works, try to find something smaller
|
||
right = mid
|
||
else:
|
||
# mid is too small, need larger limit
|
||
left = mid + 1
|
||
|
||
return left
|
||
explanation: |
|
||
**Time Complexity:** O(n × log(S)) where S = sum(nums) - max(nums) — binary search with O(n) validation per iteration.
|
||
|
||
**Space Complexity:** O(1) — only constant extra space used.
|
||
|
||
We binary search over possible answers. For each candidate `mid`, we greedily check if the array can be split into at most `k` subarrays where each has sum ≤ `mid`. The greedy approach works because if we can fit more elements in the current subarray without exceeding `mid`, we should — this minimizes the number of subarrays needed.
|
||
|
||
- approach_name: Dynamic Programming
|
||
is_optimal: false
|
||
code: |
|
||
def splitArray(nums: list[int], k: int) -> int:
|
||
n = len(nums)
|
||
|
||
# Precompute prefix sums for O(1) range sum queries
|
||
prefix = [0] * (n + 1)
|
||
for i in range(n):
|
||
prefix[i + 1] = prefix[i] + nums[i]
|
||
|
||
# dp[i][j] = min largest sum to split nums[0:i] into j parts
|
||
dp = [[float('inf')] * (k + 1) for _ in range(n + 1)]
|
||
dp[0][0] = 0 # Base case: empty array, 0 parts
|
||
|
||
for i in range(1, n + 1):
|
||
for j in range(1, min(i, k) + 1):
|
||
# Try all possible last subarray starting points
|
||
for m in range(j - 1, i):
|
||
# Sum of nums[m:i] = prefix[i] - prefix[m]
|
||
last_sum = prefix[i] - prefix[m]
|
||
# Max of (best for first m elements in j-1 parts) and last subarray
|
||
dp[i][j] = min(dp[i][j], max(dp[m][j - 1], last_sum))
|
||
|
||
return dp[n][k]
|
||
explanation: |
|
||
**Time Complexity:** O(n² × k) — three nested loops over positions and partitions.
|
||
|
||
**Space Complexity:** O(n × k) — the DP table.
|
||
|
||
This DP approach defines `dp[i][j]` as the minimum largest subarray sum when splitting the first `i` elements into exactly `j` parts. For each state, we try all possible positions for the last cut.
|
||
|
||
While correct, this is slower than binary search for large inputs. It's useful for understanding the problem structure and can be optimized further with monotonic queue techniques.
|