Find Maximum Non-decreasing Array Length
HardPrompt
You are given a 0-indexed integer array nums.
You can perform any number of operations, where each operation involves selecting a subarray of the array and replacing it with the sum of its elements. For example, if the given array is [1,3,5,6] and you select subarray [3,5] the array will convert to [1,8,6].
Return the maximum length of a non-decreasing array that can be made after applying operations.
A subarray is a contiguous non-empty sequence of elements within an array.
Example 1:
Input: nums = [5,2,2]
Output: 1
Explanation: This array with length 3 is not non-decreasing.
We have two ways to make the array length two.
First, choosing subarray [2,2] converts the array to [5,4].
Second, choosing subarray [5,2] converts the array to [7,2].
In these two ways the array is not non-decreasing.
And if we choose subarray [5,2,2] and replace it with [9] it becomes non-decreasing.
So the answer is 1.Example 2:
Input: nums = [1,2,3,4]
Output: 4
Explanation: The array is non-decreasing. So the answer is 4.Example 3:
Input: nums = [4,3,2,6]
Output: 3
Explanation: Replacing [3,2] with [5] converts the given array to [4,5,6] that is non-decreasing.
Because the given array is not non-decreasing, the maximum possible answer is 3.
Constraints:
1 <= nums.length <= 1051 <= nums[i] <= 105
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses dynamic programming to solve the problem. We define a DP state dp[i] as the maximum length of a non-decreasing array that can be formed from the prefix nums[0...i-1]. To ensure we can extend our non-decreasing sequence, we also need to keep track of the value of the last element. To maximize our chances for future extensions, we should aim for the smallest possible last element. Therefore, we use another array, last[i], to store the minimum sum of the last segment for an array of length dp[i].
The transition involves iterating through all possible previous states. For each i, we consider every possible split point j < i. This means we take an optimal solution for the prefix nums[0...j-1] and append a new element formed by summing the subarray nums[j...i-1]. If this new element is greater than or equal to the last element of the solution for nums[0...j-1], we have a valid candidate for the solution for nums[0...i-1]. We take the best among all such candidates.
Algorithm
- Create a prefix sum array
prefixto quickly calculate the sum of any subarray.prefix[i]will store the sum ofnums[0...i-1]. - Initialize two arrays,
dp[n+1]andlast[n+1].dp[i]will store the maximum length of a non-decreasing array using the prefixnums[0...i-1], andlast[i]will store the minimum possible sum of the last element for that length. - Set base cases:
dp[0] = 0andlast[0] = 0for an empty prefix. - Iterate from
i = 1ton. For eachi, we want to computedp[i]andlast[i]. - To do this, iterate through all possible split points
jfrom0toi-1. A split atjmeans the last segment of our new array is the sum ofnums[j...i-1], and the previous part is an optimal arrangement ofnums[0...j-1]. - The sum of the new last segment is
currentSum = prefix[i] - prefix[j]. - For the new array to be non-decreasing,
currentSummust be greater than or equal tolast[j]. - If the condition
currentSum >= last[j]holds, we can form a new array of lengthdp[j] + 1. - We update
dp[i]andlast[i]to get the maximum possible length and the minimum last sum for that length:- If
dp[j] + 1 > dp[i], we've found a longer array. Updatedp[i] = dp[j] + 1andlast[i] = currentSum. - If
dp[j] + 1 == dp[i], we've found an array of the same maximum length. We updatelast[i] = min(last[i], currentSum)to make it easier for future elements.
- If
- After the loops complete,
dp[n]will hold the maximum length for the entire array.
Walkthrough
The core of this method is a nested loop structure. The outer loop iterates through each possible end position i of a prefix of nums, from 1 to n. The inner loop iterates through all possible split points j for that prefix. For each pair (i, j), we calculate the sum of the subarray nums[j...i-1] and check if it can extend the non-decreasing array ending at j-1.
To efficiently calculate subarray sums, we precompute a prefix sum array. prefix[k] = nums[0] + ... + nums[k-1]. The sum of nums[j...i-1] is then prefix[i] - prefix[j].
Here is the implementation in Java:
class Solution { public int findMaximumLength(int[] nums) { int n = nums.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + nums[i]; } int[] dp = new int[n + 1]; long[] last = new long[n + 1]; // dp[0] = 0, last[0] = 0 for an empty prefix for (int i = 1; i <= n; i++) { // Initialize with a default value before checking predecessors dp[i] = 0; last[i] = Long.MAX_VALUE; for (int j = 0; j < i; j++) { long currentSum = prefix[i] - prefix[j]; if (currentSum >= last[j]) { int newLength = dp[j] + 1; if (newLength > dp[i]) { dp[i] = newLength; last[i] = currentSum; } else if (newLength == dp[i]) { last[i] = Math.min(last[i], currentSum); } } } } return dp[n]; }}Complexity
Time
O(N^2), where N is the length of `nums`. The two nested loops for `i` and `j` lead to a quadratic number of operations.
Space
O(N), where N is the length of `nums`. We use arrays `prefix`, `dp`, and `last`, each of size N+1.
Trade-offs
Pros
It is a straightforward and relatively easy-to-understand application of dynamic programming.
The logic directly follows the problem definition.
Cons
The O(N^2) time complexity is too slow for the given constraints (N up to 10^5), leading to a Time Limit Exceeded (TLE) error on larger test cases.
The O(N^2) dynamic programming approach is too slow because of the inner loop that searches for the best predecessor j for each i. This search takes O(N) time. We can optimize this search to O(log N) by maintaining a special data structure.
The key observation is that we are looking for a predecessor j that satisfies a condition (cost[j] <= prefix[i]) and maximizes a value (dp[j], prefix[j]). Many previous candidates j might be suboptimal. For instance, if we have two candidates j1 and j2 where j1 is better or equal in all aspects (lower cost, higher or equal length), j2 will never be the optimal choice.
This suggests maintaining a list of only the 'best' or non-dominated candidates. This list, which we call a monotonic candidate list M, will store indices j such that as we traverse M, their cost values and their (dp, prefix) values are both strictly increasing. With this sorted structure, we can use binary search to find the best predecessor for i in O(log N) time. The list itself can be updated in amortized constant time, leading to an overall O(N log N) solution.
Algorithm
- As with the previous approach, precompute the
prefixsum array. - We define
dp[i](max length) andlast[i](min last sum) as before. We also definecost[j] = prefix[j] + last[j]. - The condition to extend a sequence from
jtoiisprefix[i] - prefix[j] >= last[j], which simplifies tocost[j] <= prefix[i]. - To find the best predecessor
jfori, we need to findj < ithat satisfiescost[j] <= prefix[i]and maximizes the pair(dp[j], prefix[j])lexicographically. Maximizingprefix[j]helps minimize the newlast[i]. - We maintain a monotonic list
Mof candidate indicesj. This list stores non-dominated candidates. A candidatej1is non-dominated if there is no other candidatej2such thatcost[j2] <= cost[j1]and(dp[j2], prefix[j2]) >= (dp[j1], prefix[j1]). - The list
Mwill have the property that bothcostand(dp, prefix)values are strictly increasing for indices inM. - For each
ifrom 1 ton:- Perform a binary search on
Mto find the rightmost indexj_bestinMsuch thatcost[j_best] <= prefix[i]. This gives the optimal predecessor. - Calculate
dp[i],last[i], andcost[i]usingj_best. - Update the monotonic list
Mby addingi. First, remove any candidates from the end ofMthat are now dominated byi. Then, ifiis not dominated by the new last element ofM, additoM.
- Perform a binary search on
- The update to
Mtakes amortized O(1) time. - The final answer is
dp[n].
Walkthrough
This optimized DP approach refines the state transition. The state variables dp[i], last[i], and the helper prefix array remain the same. We introduce cost[j] = prefix[j] + last[j] to simplify the condition for a valid predecessor.
The main improvement comes from how we find the optimal predecessor j. Instead of a linear scan, we use a list M that stores indices of pareto-optimal candidates. For an index i to be added to M, it must not be 'dominated' by any existing point in M. An index k dominates j if it's better on all criteria: cost[k] <= cost[j] and (dp[k], prefix[k]) >= (dp[j], prefix[j]) lexicographically.
By maintaining M such that cost and (dp, prefix) are strictly increasing, we can efficiently query it. For each i, we binary search M for the candidate j_best with the largest cost not exceeding prefix[i]. This j_best will also have the maximal (dp, prefix) pair among all valid candidates.
Here is the Java implementation:
import java.util.ArrayList;import java.util.List; class Solution { public int findMaximumLength(int[] nums) { int n = nums.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + nums[i]; } int[] dp = new int[n + 1]; long[] cost = new long[n + 1]; // cost[j] = prefix[j] + last[j] // Monotonic list of candidate indices List<Integer> m = new ArrayList<>(); m.add(0); // Base case: empty prefix for (int i = 1; i <= n; i++) { long currentPrefix = prefix[i]; // Binary search for the best predecessor j_best in m int low = 0, high = m.size() - 1; int j_best = 0; // Default to 0 if no other candidate is valid while (low <= high) { int midIdx = low + (high - low) / 2; int mid_j = m.get(midIdx); if (cost[mid_j] <= currentPrefix) { j_best = mid_j; low = midIdx + 1; } else { high = midIdx - 1; } } dp[i] = dp[j_best] + 1; long last_i = currentPrefix - prefix[j_best]; cost[i] = currentPrefix + last_i; // Update the monotonic candidate list m // Remove candidates from the end that are dominated by i while (!m.isEmpty()) { int j_back = m.get(m.size() - 1); boolean costDom = cost[i] <= cost[j_back]; boolean valDom = dp[i] > dp[j_back] || (dp[i] == dp[j_back] && prefix[i] >= prefix[j_back]); if (costDom && valDom) { m.remove(m.size() - 1); } else { break; } } // Check if i is dominated by the new last element of m boolean isDominated = false; if (!m.isEmpty()) { int j_back = m.get(m.size() - 1); boolean costDom = cost[j_back] <= cost[i]; boolean valDom = dp[j_back] > dp[i] || (dp[j_back] == dp[i] && prefix[j_back] >= prefix[i]); if (costDom && valDom) { isDominated = true; } } if (!isDominated) { m.add(i); } } return dp[n]; }}Complexity
Time
O(N log N). For each of the N elements, we perform a binary search on the monotonic list `M`, which takes O(log N) time. The update to `M` involves a while loop that, over all iterations, removes each element at most once, resulting in an amortized O(1) time per update.
Space
O(N), for the `prefix`, `dp`, `cost` arrays, and the monotonic list `M` which can store up to N+1 indices in the worst case.
Trade-offs
Pros
Highly efficient with O(N log N) time complexity, which passes the given constraints.
Optimizes the search for the best predecessor from linear to logarithmic time.
Cons
The logic is significantly more complex than the O(N^2) DP approach.
Implementation requires careful handling of the monotonic candidate list and domination rules.
Solutions
Solution
class Solution {public int findMaximumLength(int[] nums) { int n = nums.length; long[] s = new long[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } int[] f = new int[n + 1]; int[] pre = new int[n + 2]; for (int i = 1; i <= n; ++i) { pre[i] = Math.max(pre[i], pre[i - 1]); f[i] = f[pre[i]] + 1; int j = Arrays.binarySearch(s, s[i] * 2 - s[pre[i]]); pre[j < 0 ? -j - 1 : j] = i; } return f[n]; }}class Solution {public: int findMaximumLength(vector<int> &nums) { int n = nums.size(); int f[n + 1]; int pre[n + 2]; long long s[n + 1]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + nums[i]; } memset(f, 0, sizeof(f)); memset(pre, 0, sizeof(pre)); for (int i = 1; i <= n; ++i) { pre[i] = max(pre[i], pre[i - 1]); f[i] = f[pre[i]] + 1; int j = lower_bound(s, s + n + 1, s[i] * 2 - s[pre[i]]) - s; pre[j] = i; } return f[n]; }};class Solution: def findMaximumLength(self, nums: List[int]) -> int: n = len(nums) s = list(accumulate(nums, initial=0)) f = [0] * (n + 1) pre = [0] * (n + 2) for i in range(1, n + 1): pre[i] = max(pre[i], pre[i - 1]) f[i] = f[pre[i]] + 1 j = bisect_left(s, s[i] * 2 - s[pre[i]]) pre[j] = i return f[n]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.