2811. Check if it is Possible to Split Array
Problem Description
You are given an array nums
of length n
and an integer m
. Your goal is to determine if you can split this array into n
separate arrays, where each final array contains exactly one element.
The splitting process follows specific rules:
A good array is defined as:
- An array with length equal to 1, OR
- An array whose sum of elements is greater than or equal to
m
The splitting process works as follows:
- Start with the original array
nums
- In each step, select any existing array that has at least 2 elements
- Split the selected array into exactly 2 new arrays
- Both resulting arrays must be "good" for the split to be valid
- Continue this process until you either:
- Successfully split everything into
n
single-element arrays (returntrue
) - Cannot make any more valid splits (return
false
)
- Successfully split everything into
For example, if nums = [2, 3, 3, 2, 3]
and m = 6
:
- You could split
[2, 3, 3, 2, 3]
into[2, 3, 3]
(sum = 8 ≥ 6, good) and[2, 3]
(sum = 5 < 6, but length > 1, need to check if splittable) - Continue splitting until all arrays have length 1
The key challenge is finding the right sequence of splits where at each step, both resulting subarrays are either single elements or have a sum ≥ m
.
Intuition
The key insight is that we need to think about this problem recursively. When we split an array into two parts, both parts must be "good" - meaning they either have length 1 or sum ≥ m
. After the split, we need to continue splitting each part independently until everything becomes single elements.
This naturally leads to a recursive approach: for any subarray [i, j]
, we try all possible split points k
and check if:
- The left part
[i, k]
is good (either single element or sum ≥m
) - The right part
[k+1, j]
is good (either single element or sum ≥m
) - Both parts can be further split successfully into single elements
The beauty of this approach is that we can use dynamic programming with memoization. If we've already computed whether a subarray [i, j]
can be successfully split, we don't need to compute it again.
To efficiently calculate sums of subarrays, we use a prefix sum array s
. This allows us to compute the sum of any subarray [i, j]
in O(1) time using s[j+1] - s[i]
.
The recursive function dfs(i, j)
answers: "Can the subarray from index i
to j
be split into single elements following the rules?"
Base case: If i == j
, we already have a single element, so return true
.
Recursive case: Try every possible split point k
between i
and j-1
. For each split:
- Check if left part is good:
k == i
(single element) ors[k+1] - s[i] >= m
- Check if right part is good:
k == j-1
(single element) ors[j+1] - s[k+1] >= m
- Recursively check if both parts can be further split:
dfs(i, k)
anddfs(k+1, j)
The answer is dfs(0, n-1)
- whether the entire array can be split into single elements.
Learn more about Greedy and Dynamic Programming patterns.
Solution Approach
The implementation uses memoization search (top-down dynamic programming) with a prefix sum array for efficient range sum queries.
Step 1: Build Prefix Sum Array
We first create a prefix sum array s
using accumulate(nums, initial=0)
:
s[i]
represents the sum of the firsti
elements- This allows us to calculate the sum of subarray
[i, j]
ass[j+1] - s[i]
in O(1) time
Step 2: Define Recursive Function with Memoization
The function dfs(i, j)
determines if the subarray nums[i..j]
can be split into single elements:
@cache # Memoization decorator to avoid recomputation
def dfs(i: int, j: int) -> bool:
Step 3: Handle Base Case
If i == j
, we have a single element, which doesn't need splitting:
if i == j: return True
Step 4: Try All Possible Split Points
For each potential split point k
in range [i, j)
, we split the array into:
- Left part:
nums[i..k]
- Right part:
nums[k+1..j]
Step 5: Check if Both Parts are Good
For each split point k
:
-
Variable
a
checks if left part is good:k == i
: single element (always good)- OR
s[k + 1] - s[i] >= m
: sum of left part ≥m
-
Variable
b
checks if right part is good:k == j - 1
: single element (always good)- OR
s[j + 1] - s[k + 1] >= m
: sum of right part ≥m
Step 6: Recursive Validation
If both parts are good (a
and b
are true), recursively check:
dfs(i, k)
: Can left part be split into single elements?dfs(k + 1, j)
: Can right part be split into single elements?
If all conditions are met for any split point, return True
.
Step 7: Return Result
The final answer is dfs(0, len(nums) - 1)
, checking if the entire array can be split.
The @cache
decorator ensures each subproblem dfs(i, j)
is computed only once, giving us O(n³) time complexity (n² possible subarrays × n split points each) and O(n²) space complexity for memoization.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with nums = [2, 1, 5, 2]
and m = 4
.
Initial Setup:
- Build prefix sum array:
s = [0, 2, 3, 8, 10]
- This allows us to calculate any subarray sum quickly
- For example: sum of
nums[1..2]
=s[3] - s[1]
=8 - 2
=6
Goal: Call dfs(0, 3)
to check if we can split the entire array into 4 single elements.
Step 1: dfs(0, 3)
- Split array [2, 1, 5, 2]
Try split point k = 0
:
- Left part:
[2]
(single element, good ✓) - Right part:
[1, 5, 2]
with sum = 8 ≥ 4 (good ✓) - Recursively check:
dfs(0, 0)
ANDdfs(1, 3)
Step 2: dfs(0, 0)
- Array [2]
- Base case:
i == j
, returnTrue
✓
Step 3: dfs(1, 3)
- Split array [1, 5, 2]
Try split point k = 1
:
- Left part:
[1]
(single element, good ✓) - Right part:
[5, 2]
with sum = 7 ≥ 4 (good ✓) - Recursively check:
dfs(1, 1)
ANDdfs(2, 3)
Step 4: dfs(1, 1)
- Array [1]
- Base case: return
True
✓
Step 5: dfs(2, 3)
- Split array [5, 2]
Try split point k = 2
:
- Left part:
[5]
(single element, good ✓) - Right part:
[2]
(single element, good ✓) - Recursively check:
dfs(2, 2)
ANDdfs(3, 3)
Step 6: dfs(2, 2)
and dfs(3, 3)
- Both are base cases, return
True
✓
Result: All recursive calls return True
, so the entire array can be successfully split!
The splitting sequence would be:
[2, 1, 5, 2]
→[2]
and[1, 5, 2]
[1, 5, 2]
→[1]
and[5, 2]
[5, 2]
→[5]
and[2]
Each split creates two "good" arrays (either single elements or sum ≥ 4), and we successfully reach 4 single-element arrays.
Solution Implementation
1from typing import List
2from functools import cache
3from itertools import accumulate
4
5
6class Solution:
7 def canSplitArray(self, nums: List[int], m: int) -> bool:
8 """
9 Determine if the array can be split into valid subarrays.
10
11 Args:
12 nums: Input array of integers
13 m: Minimum sum threshold for valid subarrays
14
15 Returns:
16 True if array can be split, False otherwise
17 """
18
19 @cache
20 def can_split_range(left: int, right: int) -> bool:
21 """
22 Check if subarray from index left to right can be validly split.
23
24 Args:
25 left: Starting index of the subarray (inclusive)
26 right: Ending index of the subarray (inclusive)
27
28 Returns:
29 True if the subarray can be split according to rules
30 """
31 # Base case: single element is always valid
32 if left == right:
33 return True
34
35 # Try all possible split points
36 for split_point in range(left, right):
37 # Check if left part is valid:
38 # Either it's a single element OR its sum >= m
39 left_part_valid = (split_point == left or
40 prefix_sum[split_point + 1] - prefix_sum[left] >= m)
41
42 # Check if right part is valid:
43 # Either it's a single element OR its sum >= m
44 right_part_valid = (split_point == right - 1 or
45 prefix_sum[right + 1] - prefix_sum[split_point + 1] >= m)
46
47 # If both parts are valid and can be recursively split, we found a solution
48 if (left_part_valid and right_part_valid and
49 can_split_range(left, split_point) and
50 can_split_range(split_point + 1, right)):
51 return True
52
53 # No valid split found
54 return False
55
56 # Build prefix sum array for efficient range sum queries
57 # prefix_sum[i] = sum of nums[0:i]
58 prefix_sum = list(accumulate(nums, initial=0))
59
60 # Check if entire array can be split
61 return can_split_range(0, len(nums) - 1)
62
1class Solution {
2 // Memoization table to store results of subproblems
3 // dp[i][j] represents whether subarray from index i to j can be split validly
4 private Boolean[][] dp;
5
6 // Prefix sum array for quick range sum calculation
7 private int[] prefixSum;
8
9 // Minimum sum threshold for valid subarrays
10 private int minSum;
11
12 public boolean canSplitArray(List<Integer> nums, int m) {
13 int n = nums.size();
14
15 // Initialize memoization table
16 dp = new Boolean[n][n];
17
18 // Build prefix sum array where prefixSum[i] = sum of first i elements
19 prefixSum = new int[n + 1];
20 for (int i = 1; i <= n; i++) {
21 prefixSum[i] = prefixSum[i - 1] + nums.get(i - 1);
22 }
23
24 this.minSum = m;
25
26 // Check if entire array can be split validly
27 return canSplit(0, n - 1);
28 }
29
30 private boolean canSplit(int left, int right) {
31 // Base case: single element is always valid
32 if (left == right) {
33 return true;
34 }
35
36 // Return memoized result if already computed
37 if (dp[left][right] != null) {
38 return dp[left][right];
39 }
40
41 // Try all possible split points
42 for (int splitPoint = left; splitPoint < right; splitPoint++) {
43 // Check if left part is valid (single element or sum >= minSum)
44 boolean isLeftValid = (splitPoint == left) ||
45 (prefixSum[splitPoint + 1] - prefixSum[left] >= minSum);
46
47 // Check if right part is valid (single element or sum >= minSum)
48 boolean isRightValid = (splitPoint == right - 1) ||
49 (prefixSum[right + 1] - prefixSum[splitPoint + 1] >= minSum);
50
51 // If both parts are valid and can be further split recursively
52 if (isLeftValid && isRightValid &&
53 canSplit(left, splitPoint) && canSplit(splitPoint + 1, right)) {
54 // Cache and return true
55 return dp[left][right] = true;
56 }
57 }
58
59 // No valid split found, cache and return false
60 return dp[left][right] = false;
61 }
62}
63
1class Solution {
2public:
3 bool canSplitArray(vector<int>& nums, int m) {
4 int n = nums.size();
5
6 // Build prefix sum array for quick range sum calculation
7 // prefixSum[i] represents sum of elements from index 0 to i-1
8 vector<int> prefixSum(n + 1);
9 for (int i = 1; i <= n; ++i) {
10 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
11 }
12
13 // Memoization table for dynamic programming
14 // memo[i][j] stores whether subarray from index i to j can be split
15 // -1: not computed, 0: false, 1: true
16 int memo[n][n];
17 memset(memo, -1, sizeof(memo));
18
19 // Recursive function with memoization to check if range [left, right] can be split
20 function<bool(int, int)> canSplit = [&](int left, int right) -> bool {
21 // Base case: single element is always valid
22 if (left == right) {
23 return true;
24 }
25
26 // Return cached result if already computed
27 if (memo[left][right] != -1) {
28 return memo[left][right] == 1;
29 }
30
31 // Try all possible split points
32 for (int splitPoint = left; splitPoint < right; ++splitPoint) {
33 // Check if left subarray is valid:
34 // - Either it's a single element (splitPoint == left)
35 // - Or its sum is >= m
36 bool leftValid = (splitPoint == left) ||
37 (prefixSum[splitPoint + 1] - prefixSum[left] >= m);
38
39 // Check if right subarray is valid:
40 // - Either it's a single element (splitPoint == right - 1)
41 // - Or its sum is >= m
42 bool rightValid = (splitPoint == right - 1) ||
43 (prefixSum[right + 1] - prefixSum[splitPoint + 1] >= m);
44
45 // If both subarrays are valid and can be further split recursively
46 if (leftValid && rightValid &&
47 canSplit(left, splitPoint) && canSplit(splitPoint + 1, right)) {
48 memo[left][right] = 1;
49 return true;
50 }
51 }
52
53 // No valid split found
54 memo[left][right] = 0;
55 return false;
56 };
57
58 // Check if the entire array can be split
59 return canSplit(0, n - 1);
60 }
61};
62
1/**
2 * Determines if an array can be split into valid subarrays based on given constraints
3 * @param nums - The input array of numbers
4 * @param m - The minimum sum threshold for valid subarrays
5 * @returns True if the array can be split successfully, false otherwise
6 */
7function canSplitArray(nums: number[], m: number): boolean {
8 const arrayLength: number = nums.length;
9
10 // Build prefix sum array for efficient range sum queries
11 // prefixSum[i] represents sum of elements from index 0 to i-1
12 const prefixSum: number[] = new Array(arrayLength + 1).fill(0);
13 for (let i = 1; i <= arrayLength; i++) {
14 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
15 }
16
17 // Memoization table for dynamic programming
18 // memo[i][j] stores whether subarray from index i to j can be split
19 // -1: not computed, 0: cannot split, 1: can split
20 const memo: number[][] = Array(arrayLength)
21 .fill(0)
22 .map(() => Array(arrayLength).fill(-1));
23
24 /**
25 * Recursively checks if subarray from startIndex to endIndex can be split
26 * @param startIndex - Starting index of the subarray
27 * @param endIndex - Ending index of the subarray
28 * @returns True if the subarray can be split according to rules
29 */
30 const checkCanSplit = (startIndex: number, endIndex: number): boolean => {
31 // Base case: single element always satisfies the condition
32 if (startIndex === endIndex) {
33 return true;
34 }
35
36 // Return memoized result if already computed
37 if (memo[startIndex][endIndex] !== -1) {
38 return memo[startIndex][endIndex] === 1;
39 }
40
41 // Try all possible split points
42 for (let splitPoint = startIndex; splitPoint < endIndex; splitPoint++) {
43 // Check if left part is valid (single element or sum >= m)
44 const isLeftPartValid: boolean =
45 splitPoint === startIndex ||
46 prefixSum[splitPoint + 1] - prefixSum[startIndex] >= m;
47
48 // Check if right part is valid (single element or sum >= m)
49 const isRightPartValid: boolean =
50 splitPoint === endIndex - 1 ||
51 prefixSum[endIndex + 1] - prefixSum[splitPoint + 1] >= m;
52
53 // If both parts are valid and can be further split recursively
54 if (isLeftPartValid &&
55 isRightPartValid &&
56 checkCanSplit(startIndex, splitPoint) &&
57 checkCanSplit(splitPoint + 1, endIndex)) {
58 memo[startIndex][endIndex] = 1;
59 return true;
60 }
61 }
62
63 // No valid split found
64 memo[startIndex][endIndex] = 0;
65 return false;
66 };
67
68 // Check if entire array can be split
69 return checkCanSplit(0, arrayLength - 1);
70}
71
Time and Space Complexity
Time Complexity: O(n³)
The analysis breaks down as follows:
- The recursive function
dfs(i, j)
uses memoization with@cache
, so each unique state(i, j)
is computed only once - There are
O(n²)
possible states sincei
can range from0
ton-1
andj
can range fromi
ton-1
- For each state
(i, j)
, the function iterates through all possible split pointsk
fromi
toj-1
, which takesO(j - i) = O(n)
time in the worst case - The operations inside the loop (checking conditions
a
andb
using prefix sums) takeO(1)
time - Therefore, the total time complexity is
O(n²) × O(n) = O(n³)
Space Complexity: O(n²)
The space usage comes from:
- The memoization cache stores results for all possible
(i, j)
pairs, which requiresO(n²)
space - The prefix sum array
s
requiresO(n)
space - The recursion stack depth can be at most
O(n)
in the worst case - Since
O(n²)
dominates, the overall space complexity isO(n²)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors in Index Handling
One of the most common mistakes in this problem is confusion between inclusive and exclusive indices when working with the prefix sum array and recursive calls.
Pitfall Example:
# Incorrect: Mixing up index boundaries right_part_valid = (split_point == right or # Wrong comparison! prefix_sum[right] - prefix_sum[split_point + 1] >= m) # Wrong index!
Why it happens: The prefix sum array has length n+1
while the original array has length n
. It's easy to confuse:
prefix_sum[i]
represents sum of firsti
elements (0 to i-1)nums[i]
represents the element at indexi
Solution: Always remember:
- For sum of subarray from index
i
toj
(inclusive): useprefix_sum[j + 1] - prefix_sum[i]
- When checking if right part is a single element after splitting at
split_point
, compare withright - 1
, notright
2. Forgetting to Check Both Split Conditions
Pitfall Example:
# Incorrect: Only checking if parts can be recursively split
for split_point in range(left, right):
if can_split_range(left, split_point) and can_split_range(split_point + 1, right):
return True # Forgot to check if parts are "good"!
Why it happens: The problem has two layers of validation:
- Each split must produce two "good" arrays (single element OR sum ≥ m)
- Each resulting array must be recursively splittable
Solution: Always validate both conditions:
if (left_part_valid and right_part_valid and # Check if parts are good can_split_range(left, split_point) and # Check recursive splitting can_split_range(split_point + 1, right)):
3. Incorrect Base Case for Arrays of Length 2
Pitfall Example:
# Incorrect: Special-casing length 2 arrays if right - left == 1: # Array of length 2 return nums[left] >= m or nums[right] >= m # Wrong logic!
Why it happens: It's tempting to add special cases for small arrays, but this can introduce bugs. An array of length 2 can only be split into two single elements, and single elements are always "good" regardless of their value.
Solution: Keep the base case simple - only check for single elements:
if left == right: return True # Let the general case handle arrays of length 2+
4. Not Using Memoization or Using It Incorrectly
Pitfall Example:
# Incorrect: Forgetting memoization leads to exponential time complexity
def can_split_range(left: int, right: int) -> bool: # No @cache decorator!
# ... recursive calls without memoization
Why it happens: Without memoization, the same subproblems are computed multiple times, leading to exponential time complexity that will cause timeout for larger inputs.
Solution: Always use memoization for overlapping subproblems:
- Use
@cache
decorator from functools - Or manually implement with a dictionary/2D array
- Ensure the function parameters are hashable (integers, not lists)
5. Edge Case: Arrays with Length ≤ 2
Pitfall Example:
# Forgetting that arrays of length 1 or 2 have special behavior nums = [5] # Length 1 - always returns True nums = [3, 2] # Length 2 - always returns True (splits into two single elements)
Why it happens: The problem statement might not explicitly clarify these cases, leading to overthinking.
Solution: Remember the fundamental rule:
- Single elements are always "good"
- Any array of length ≤ 2 can always be split into single elements
- The algorithm handles these naturally without special cases
How does quick sort divide the problem into subproblems?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!