2945. Find Maximum Non-decreasing Array Length
Problem Description
You are given a 0-indexed integer array nums
.
You can perform any number of operations on this array. In each operation, you:
- Select a subarray (a contiguous non-empty sequence of elements)
- Replace that subarray with a single element equal to the sum of all elements in that subarray
For example, if nums = [1,3,5,6]
and you select the subarray [3,5]
, the array becomes [1,8,6]
(the subarray [3,5]
is replaced by their sum 8
).
Your goal is to find the maximum length of a non-decreasing array that can be obtained after performing any number of these operations.
A non-decreasing array means each element is greater than or equal to the previous element (i.e., nums[i] <= nums[i+1]
for all valid indices).
Examples:
-
nums = [5,2,2]
: The original array is not non-decreasing. You can:- Replace
[2,2]
with[4]
to get[5,4]
- still not non-decreasing - Replace
[5,2]
with[7]
to get[7,2]
- still not non-decreasing - Replace
[5,2,2]
with[9]
to get[9]
- this is non-decreasing with length 1 - Maximum achievable length is 1
- Replace
-
nums = [1,2,3,4]
: Already non-decreasing, so maximum length is 4 -
nums = [4,3,2,6]
: Replace[3,2]
with[5]
to get[4,5,6]
which is non-decreasing with length 3
The challenge is to strategically choose which subarrays to merge to create the longest possible non-decreasing sequence.
Intuition
The key insight is that when we merge subarrays into their sum, we're essentially creating segments where each segment has a value (the sum of its elements). To maximize the length of the final non-decreasing array, we want to create as many segments as possible while maintaining the non-decreasing property.
Think about it this way: if we have a segment ending at position i
with sum S
, the next segment starting from position i+1
must have a sum at least S
to maintain the non-decreasing property. This means we want to find the shortest possible next segment whose sum is at least S
.
This leads us to a dynamic programming approach where:
f[i]
represents the maximum number of segments we can create using the firsti
elements- For each position
i
, we need to track what was the last position where we ended a segment that allows us to create valid segments up to positioni
The crucial observation is that if we ended a segment at position pre[i]
, and the sum from pre[i]
to i
is sum1
, then the next segment must have sum at least sum1
. To find where the next segment can end, we need to find the smallest index j
where the sum from i
to j
is at least sum1
.
Using prefix sums s[i]
to quickly calculate range sums:
- Sum from position
a
tob
iss[b] - s[a]
- If the last segment ended at
pre[i]
with sums[i] - s[pre[i]]
, we need the next segment to have sum at least this value - We're looking for the smallest
j
wheres[j] - s[i] >= s[i] - s[pre[i]]
- Rearranging:
s[j] >= 2*s[i] - s[pre[i]]
This is why the solution uses bisect_left
to find the earliest position where we can end the next segment, and maintains pre[j]
to track the best previous segment ending position for each index.
Learn more about Stack, Queue, Binary Search, Dynamic Programming, Monotonic Queue and Monotonic Stack patterns.
Solution Approach
The solution uses dynamic programming with prefix sums and binary search to efficiently find the maximum number of segments (which equals the maximum length of the final non-decreasing array).
Data Structures Used:
s
: Prefix sum array wheres[i]
= sum ofnums[0]
tonums[i-1]
f[i]
: Maximum number of segments achievable using the firsti
elementspre[i]
: The best ending position of the last segment before positioni
Algorithm Steps:
-
Initialize prefix sums: Create
s
usingaccumulate(nums, initial=0)
so thats[i]
stores the sum of elements from index 0 to i-1. -
Dynamic Programming Loop: For each position
i
from 1 to n:a. Inherit the best previous position:
pre[i] = max(pre[i], pre[i-1])
- This ensures we're using the optimal ending position from previous computations
b. Calculate maximum segments:
f[i] = f[pre[i]] + 1
- We can form one more segment than what we had at position
pre[i]
- The new segment spans from
pre[i]
toi
c. Find next valid position: Use binary search to find the smallest index
j
where we can end the next segment- The sum of the current segment is
s[i] - s[pre[i]]
- The next segment starting at
i
must have sum ≥s[i] - s[pre[i]]
- We need
s[j] - s[i] >= s[i] - s[pre[i]]
, which gives uss[j] >= 2*s[i] - s[pre[i]]
j = bisect_left(s, 2*s[i] - s[pre[i]])
d. Update future position:
pre[j] = i
- Record that position
i
is a good ending position for creating segments up to positionj
-
Return Result:
f[n]
contains the maximum number of segments using all n elements.
Why this works:
- By maintaining
pre[i]
, we always know the optimal way to partition elements up to positioni
- The binary search ensures we find the earliest possible position to end the next segment while maintaining the non-decreasing property
- The DP recurrence
f[i] = f[pre[i]] + 1
builds up the solution optimally from smaller subproblems
Time Complexity: O(n log n) due to the binary search operation for each of the n positions. Space Complexity: O(n) for the auxiliary arrays.
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 the solution with nums = [4,3,2,6]
.
Step 1: Initialize prefix sums
nums = [4,3,2,6]
s = [0,4,7,9,15]
(cumulative sums)n = 4
f = [0,0,0,0,0]
(max segments for each position)pre = [0,0,0,0,0]
(best previous segment ending)
Step 2: Process each position
i = 1:
pre[1] = max(0, 0) = 0
f[1] = f[0] + 1 = 0 + 1 = 1
(one segment from 0 to 1)- Current segment sum:
s[1] - s[0] = 4 - 0 = 4
- Find next position j where sum ≥ 4:
- Need
s[j] >= 2*4 - 0 = 8
bisect_left(s, 8)
returns 3 (since s[3] = 9 ≥ 8)
- Need
pre[3] = 1
(position 1 is good for segments up to position 3)
i = 2:
pre[2] = max(0, 0) = 0
f[2] = f[0] + 1 = 1
(one segment from 0 to 2)- Current segment sum:
s[2] - s[0] = 7 - 0 = 7
- Find next position j where sum ≥ 7:
- Need
s[j] >= 2*7 - 0 = 14
bisect_left(s, 14)
returns 4 (since s[4] = 15 ≥ 14)
- Need
pre[4] = 2
(position 2 is good for segments up to position 4)
i = 3:
pre[3] = max(1, 0) = 1
(from earlier update)f[3] = f[1] + 1 = 1 + 1 = 2
(two segments: [4] and [3,2])- Current segment sum:
s[3] - s[1] = 9 - 4 = 5
- Find next position j where sum ≥ 5:
- Need
s[j] >= 2*9 - 4 = 14
bisect_left(s, 14)
returns 4
- Need
pre[4] = max(2, 3) = 3
(update to better position)
i = 4:
pre[4] = max(3, 0) = 3
(from earlier updates)f[4] = f[3] + 1 = 2 + 1 = 3
(three segments)- Current segment sum:
s[4] - s[3] = 15 - 9 = 6
- Find next position j where sum ≥ 6:
- Need
s[j] >= 2*15 - 9 = 21
bisect_left(s, 21)
returns 5 (beyond array)
- Need
- No update needed (j > n)
Result: f[4] = 3
This means we can create 3 segments:
- Segment 1:
[4]
with sum 4 - Segment 2:
[3,2]
with sum 5 - Segment 3:
[6]
with sum 6
The final array [4,5,6]
is non-decreasing with length 3.
Solution Implementation
1from typing import List
2from itertools import accumulate
3from bisect import bisect_left
4
5
6class Solution:
7 def findMaximumLength(self, nums: List[int]) -> int:
8 """
9 Find the maximum length of a non-decreasing sequence that can be formed
10 by merging consecutive elements of the input array.
11
12 Args:
13 nums: List of integers representing the input array
14
15 Returns:
16 Maximum length of the resulting sequence
17 """
18 n = len(nums)
19
20 # Calculate prefix sums for quick range sum queries
21 # prefix_sums[i] = sum of nums[0:i]
22 prefix_sums = list(accumulate(nums, initial=0))
23
24 # dp[i] represents the maximum length of sequence using nums[0:i]
25 dp = [0] * (n + 1)
26
27 # last_segment_start[i] stores the optimal starting position of the last segment
28 # when considering elements up to position i
29 last_segment_start = [0] * (n + 2)
30
31 # Process each position from 1 to n
32 for i in range(1, n + 1):
33 # Update the best starting position seen so far
34 last_segment_start[i] = max(last_segment_start[i], last_segment_start[i - 1])
35
36 # Calculate maximum length at position i
37 # We extend from the optimal previous segment
38 dp[i] = dp[last_segment_start[i]] + 1
39
40 # Find the next position where we can form a valid segment
41 # The segment from last_segment_start[i] to i has sum:
42 # prefix_sums[i] - prefix_sums[last_segment_start[i]]
43 # We need the next segment to have at least this sum
44 segment_sum = prefix_sums[i] - prefix_sums[last_segment_start[i]]
45 target_prefix_sum = prefix_sums[i] + segment_sum
46 next_position = bisect_left(prefix_sums, target_prefix_sum)
47
48 # Update the optimal starting position for the next valid position
49 last_segment_start[next_position] = i
50
51 return dp[n]
52
1class Solution {
2 public int findMaximumLength(int[] nums) {
3 int n = nums.length;
4
5 // Build prefix sum array where prefixSum[i] = sum of nums[0] to nums[i-1]
6 long[] prefixSum = new long[n + 1];
7 for (int i = 0; i < n; ++i) {
8 prefixSum[i + 1] = prefixSum[i] + nums[i];
9 }
10
11 // dp[i] represents the maximum number of segments we can form using elements from index 0 to i-1
12 int[] dp = new int[n + 1];
13
14 // lastSegmentEnd[i] stores the ending index of the last segment when considering elements up to index i-1
15 // This helps track where the previous valid segment ended for optimal partitioning
16 int[] lastSegmentEnd = new int[n + 2];
17
18 // Process each position from 1 to n
19 for (int i = 1; i <= n; ++i) {
20 // Propagate the best previous segment ending position
21 // This ensures we always have the optimal previous segment available
22 lastSegmentEnd[i] = Math.max(lastSegmentEnd[i], lastSegmentEnd[i - 1]);
23
24 // Calculate the maximum segments: take the best result from the optimal previous position and add 1
25 dp[i] = dp[lastSegmentEnd[i]] + 1;
26
27 // Find the next position where we can potentially start a new segment
28 // We're looking for position j where sum[j] - sum[i] >= sum[i] - sum[lastSegmentEnd[i]]
29 // This translates to: sum[j] >= 2 * sum[i] - sum[lastSegmentEnd[i]]
30 long targetSum = prefixSum[i] * 2 - prefixSum[lastSegmentEnd[i]];
31 int nextPosition = Arrays.binarySearch(prefixSum, targetSum);
32
33 // If exact match not found, binarySearch returns -(insertion point) - 1
34 // Convert to the actual insertion point (next valid position)
35 if (nextPosition < 0) {
36 nextPosition = -nextPosition - 1;
37 }
38
39 // Update the last segment end for this future position
40 // This position i becomes a candidate for being the end of a segment when we reach nextPosition
41 lastSegmentEnd[nextPosition] = i;
42 }
43
44 // Return the maximum number of segments we can form using all n elements
45 return dp[n];
46 }
47}
48
1class Solution {
2public:
3 int findMaximumLength(vector<int>& nums) {
4 int n = nums.size();
5
6 // dp[i] = maximum number of segments we can create using first i elements
7 vector<int> dp(n + 1, 0);
8
9 // lastSegmentEnd[i] = the ending position of the last segment when considering first i elements
10 vector<int> lastSegmentEnd(n + 2, 0);
11
12 // prefixSum[i] = sum of first i elements (1-indexed)
13 vector<long long> prefixSum(n + 1, 0);
14
15 // Calculate prefix sums
16 for (int i = 0; i < n; ++i) {
17 prefixSum[i + 1] = prefixSum[i] + nums[i];
18 }
19
20 // Dynamic programming approach
21 for (int i = 1; i <= n; ++i) {
22 // Propagate the best ending position from previous positions
23 lastSegmentEnd[i] = max(lastSegmentEnd[i], lastSegmentEnd[i - 1]);
24
25 // Calculate maximum segments ending at position i
26 // We add 1 segment to the optimal solution at lastSegmentEnd[i]
27 dp[i] = dp[lastSegmentEnd[i]] + 1;
28
29 // Find the next position where we can start a new segment
30 // The sum of the next segment should be at least as large as current segment
31 // Current segment sum: prefixSum[i] - prefixSum[lastSegmentEnd[i]]
32 // Next segment should start at position j where:
33 // prefixSum[j] - prefixSum[i] >= prefixSum[i] - prefixSum[lastSegmentEnd[i]]
34 // Rearranging: prefixSum[j] >= 2 * prefixSum[i] - prefixSum[lastSegmentEnd[i]]
35 long long targetSum = 2 * prefixSum[i] - prefixSum[lastSegmentEnd[i]];
36 int nextPosition = lower_bound(prefixSum.begin(), prefixSum.end(), targetSum) - prefixSum.begin();
37
38 // Update the best ending position for nextPosition
39 lastSegmentEnd[nextPosition] = i;
40 }
41
42 // Return the maximum number of segments using all n elements
43 return dp[n];
44 }
45};
46
1/**
2 * Finds the maximum length of a valid partition of the array
3 * where each partition has sum greater than or equal to the previous partition
4 * @param nums - Input array of positive integers
5 * @returns Maximum number of partitions possible
6 */
7function findMaximumLength(nums: number[]): number {
8 const arrayLength: number = nums.length;
9
10 // dp[i] represents the maximum number of partitions for the first i elements
11 const dp: number[] = Array(arrayLength + 1).fill(0);
12
13 // lastPartitionEnd[i] represents the ending index of the last partition
14 // when considering elements up to index i
15 const lastPartitionEnd: number[] = Array(arrayLength + 2).fill(0);
16
17 // prefixSum[i] represents the sum of the first i elements
18 const prefixSum: number[] = Array(arrayLength + 1).fill(0);
19
20 // Calculate prefix sums for quick range sum queries
21 for (let i = 1; i <= arrayLength; ++i) {
22 prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
23 }
24
25 /**
26 * Binary search to find the leftmost index where value >= target
27 * @param searchArray - Array to search in
28 * @param target - Target value to search for
29 * @returns Index of the first element >= target
30 */
31 const binarySearch = (searchArray: number[], target: number): number => {
32 let left: number = 0;
33 let right: number = searchArray.length;
34
35 while (left < right) {
36 const mid: number = (left + right) >> 1;
37 if (searchArray[mid] >= target) {
38 right = mid;
39 } else {
40 left = mid + 1;
41 }
42 }
43 return left;
44 };
45
46 // Dynamic programming to find maximum partitions
47 for (let i = 1; i <= arrayLength; ++i) {
48 // Propagate the best partition ending from previous positions
49 lastPartitionEnd[i] = Math.max(lastPartitionEnd[i], lastPartitionEnd[i - 1]);
50
51 // Calculate maximum partitions ending at position i
52 dp[i] = dp[lastPartitionEnd[i]] + 1;
53
54 // Find the next position where we can start a new partition
55 // The sum of the next partition must be >= current partition sum
56 const nextValidPosition: number = binarySearch(
57 prefixSum,
58 prefixSum[i] * 2 - prefixSum[lastPartitionEnd[i]]
59 );
60
61 // Update the last partition ending for the next valid position
62 lastPartitionEnd[nextValidPosition] = i;
63 }
64
65 return dp[arrayLength];
66}
67
Time and Space Complexity
Time Complexity: O(n log n)
The algorithm iterates through the array once with a loop from 1 to n, which takes O(n)
time. Within each iteration:
- Accessing and updating arrays
pre
andf
takesO(1)
time - The
bisect_left
operation performs binary search on the prefix sum arrays
, which takesO(log n)
time
Since we perform the binary search operation for each of the n iterations, the overall time complexity is O(n) * O(log n) = O(n log n)
.
Space Complexity: O(n)
The algorithm uses several auxiliary arrays:
s
: prefix sum array of sizen + 1
f
: DP array of sizen + 1
pre
: array of sizen + 2
Each array uses linear space proportional to the input size n. Therefore, the total space complexity is O(n) + O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors with Index Management
The most common pitfall in this solution is confusion around the indexing conventions, particularly with the last_segment_start
array and how it relates to the prefix sums.
The Problem:
- The prefix sum array has length
n+1
whereprefix_sums[i]
represents the sum of elements from index 0 to i-1 - The
last_segment_start
array needs to handle positions beyondn
(hence sizen+2
) - When using
bisect_left
, the returned index might ben+1
, which would cause an index out of bounds error if not handled properly
Example of the pitfall:
# Incorrect: Array too small last_segment_start = [0] * (n + 1) # This will cause IndexError # When bisect_left returns n+1, last_segment_start[n+1] will fail
Solution:
Always ensure the last_segment_start
array has size n+2
to accommodate all possible positions returned by bisect_left
.
2. Incorrect Prefix Sum Calculation
The Problem: Using the wrong initial value or incorrect accumulation can lead to wrong segment sums.
Example of the pitfall:
# Incorrect: No initial value
prefix_sums = list(accumulate(nums)) # Length is n, not n+1
# This makes prefix_sums[0] = nums[0] instead of 0
Solution:
Always use accumulate(nums, initial=0)
to ensure prefix_sums[0] = 0
and the array has length n+1
.
3. Misunderstanding the Binary Search Target
The Problem:
The binary search looks for the minimum position where we can end the next segment while maintaining the non-decreasing property. The calculation 2 * prefix_sums[i] - prefix_sums[last_segment_start[i]]
might seem arbitrary.
Example of the pitfall:
# Incorrect understanding might lead to: next_position = bisect_left(prefix_sums, prefix_sums[i] + segment_sum, i+1) # Adding a start parameter to bisect_left, which changes the behavior
Solution:
Remember that we're searching for the first position j
where:
- The sum from position
i
toj-1
is at leastsegment_sum
- This translates to:
prefix_sums[j] >= prefix_sums[i] + segment_sum
- Which simplifies to:
prefix_sums[j] >= 2 * prefix_sums[i] - prefix_sums[last_segment_start[i]]
4. Not Propagating the Best Previous Position
The Problem:
Forgetting to update last_segment_start[i]
with the maximum of itself and the previous position.
Example of the pitfall:
# Incorrect: Missing the max operation
for i in range(1, n + 1):
# last_segment_start[i] = max(last_segment_start[i], last_segment_start[i - 1]) # Missing!
dp[i] = dp[last_segment_start[i]] + 1 # Will use 0 instead of optimal position
Solution:
Always include the line last_segment_start[i] = max(last_segment_start[i], last_segment_start[i - 1])
to ensure we're using the best ending position discovered so far.
Which of the following array represent a max heap?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!