LeetCode 2263 - Make Array Non-decreasing or Non-increasing
The problem asks us to transform a given integer array nums into either a non-decreasing or a non-increasing array with the minimum number of operations. Each operation allows increasing or decreasing an element by exactly 1.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming, Greedy, Heap (Priority Queue)
Solution
Problem Understanding
The problem asks us to transform a given integer array nums into either a non-decreasing or a non-increasing array with the minimum number of operations. Each operation allows increasing or decreasing an element by exactly 1.
A non-decreasing array is one in which every element is greater than or equal to the previous element, i.e., nums[i] <= nums[i+1]. A non-increasing array is one in which every element is less than or equal to the previous element, i.e., nums[i] >= nums[i+1].
The input is a list of integers between 0 and 1000, and its length can go up to 1000. The output is a single integer representing the minimum total number of increment/decrement operations needed to satisfy either of the two monotone conditions.
Important edge cases to consider include:
- Arrays of length 1 (already monotone, 0 operations needed).
- Arrays that are already non-decreasing or non-increasing (0 operations needed).
- Arrays where all elements are equal (already monotone).
- Arrays with alternating high/low patterns, which might need multiple adjustments.
The key challenge is that each element can be adjusted individually, but we must consider the global order constraint when choosing the target values.
Approaches
Brute Force
A brute-force approach would attempt to check every possible final array configuration that is non-decreasing or non-increasing. For each element, we could try all possible values from 0 to 1000 and calculate the number of operations required. We would then select the configuration with the minimum total operations. This approach is correct but extremely inefficient, with a time complexity of O(n * 1000^n), which is infeasible for n up to 1000.
Optimal Approach
The key observation is that we can model this as a Dynamic Programming (DP) problem where the state represents the minimum number of operations needed to make the first i elements monotone if the i-th element is set to a specific value. Since the values of nums[i] are bounded (0 to 1000), we can limit our DP table to these values.
- For non-decreasing arrays, we can iterate from left to right and, for each possible value of
nums[i], choose the minimum operation cost considering all smaller or equal previous values. - For non-increasing arrays, we can do the same but reverse the direction (or negate the array to reuse the same logic).
This is equivalent to solving a monotone DP problem with value compression from 0 to 1000. The DP table size is manageable (n * 1001) for the given constraints, making this approach feasible.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(1000^n) | O(1) | Try all possible final values for each element |
| Optimal (DP) | O(n * maxVal) | O(maxVal) | Use DP to track min operations for each possible element value (maxVal = 1001) |
Algorithm Walkthrough
- Initialize a DP array of size
maxVal = 1001, wheredp[v]represents the minimum operations needed to make the array non-decreasing up to the current index with the last element beingv. - Iterate through each element
numinnums. - For each possible target value
vfrom 0 to 1000:
- Update
dp[v]as the sum ofabs(num - v)plus the minimumdp[u]for allu <= v. - This ensures the array remains non-decreasing.
- After processing all elements, the answer for non-decreasing transformation is
min(dp). - To handle non-increasing arrays, either reverse the array or negate values and reuse the same DP logic.
- Return the minimum of non-decreasing and non-increasing results.
Why it works: The DP array maintains the invariant that at each step, dp[v] is the minimal total operations for a valid monotone array ending with value v. By considering all possible last values and combining with previous minima, we guarantee the global minimum for the array.
Python Solution
from typing import List
import sys
class Solution:
def convertArray(self, nums: List[int]) -> int:
def min_operations(arr: List[int]) -> int:
max_val = 1001
dp = [0] * max_val
for i, num in enumerate(arr):
new_dp = [0] * max_val
min_so_far = sys.maxsize
for v in range(max_val):
min_so_far = min(min_so_far, dp[v])
new_dp[v] = min_so_far + abs(num - v)
dp = new_dp
return min(dp)
inc_ops = min_operations(nums)
dec_ops = min_operations(nums[::-1])
return min(inc_ops, dec_ops)
Explanation: We define a helper min_operations that calculates the minimum operations to make an array non-decreasing. For each element, we iterate over all possible values and use a running minimum (min_so_far) to efficiently track the best previous cost that keeps the array monotone. We compute this for both the original array (non-decreasing) and the reversed array (non-increasing) and return the smaller result.
Go Solution
import (
"math"
)
func convertArray(nums []int) int {
maxVal := 1001
minOperations := func(arr []int) int {
dp := make([]int, maxVal)
for i := range arr {
newDp := make([]int, maxVal)
minSoFar := math.MaxInt32
for v := 0; v < maxVal; v++ {
if dp[v] < minSoFar {
minSoFar = dp[v]
}
newDp[v] = minSoFar + abs(arr[i]-v)
}
dp = newDp
}
minOps := math.MaxInt32
for _, val := range dp {
if val < minOps {
minOps = val
}
}
return minOps
}
incOps := minOperations(nums)
decOps := minOperations(reverse(nums))
if incOps < decOps {
return incOps
}
return decOps
}
func abs(a int) int {
if a < 0 {
return -a
}
return a
}
func reverse(arr []int) []int {
n := len(arr)
res := make([]int, n)
for i := 0; i < n; i++ {
res[i] = arr[n-1-i]
}
return res
}
Go-specific notes: Go requires manual handling of abs and array reversal. We also handle math.MaxInt32 for initialization of running minima.
Worked Examples
Example 1: nums = [3,2,4,5,0]
Step-by-step for non-decreasing DP:
| i | num | dp[0..5] sample | min_so_far | new_dp[0..5] sample |
|---|---|---|---|---|
| 0 | 3 | 0 | 0 | 3 |
| 1 | 2 | prev dp | 3 | 2+3=5 |
| 2 | 4 | ... | ... | ... |
After DP, min(dp) = 4 for non-increasing.
Result: 4 operations.
Example 2: nums = [2,2,3,4]
Already non-decreasing, min(dp) = 0
Example 3: nums = [0]
Single element, min(dp) = 0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * 1001) | For each of n elements, we iterate over max value range 0-1000 |
| Space | O(1001) | We only keep current DP array of size 1001 |
The algorithm efficiently reduces the problem to DP over bounded value range rather than trying all permutations.
Test Cases
# Basic examples
assert Solution().convertArray([3,2,4,5,0]) == 4 # Example 1
assert Solution().convertArray([2,2,3,4]) == 0 # Example 2
assert Solution().convertArray([0]) == 0 # Example 3
# Edge cases
assert Solution().convertArray([1,1,1,1]) == 0 # Already monotone
assert Solution().convertArray([5,4,3,2,1]) == 0 # Already non-increasing
assert Solution().convertArray([1,1000,1,1000]) == 1998 # Alternating extremes
assert Solution().convertArray([0,500,1000]) == 500 # Need small adjustments
| Test | Why |
|---|---|
| [3,2,4,5,0] | Typical case requiring adjustments |
| [2,2,3,4] | Already non-decreasing |
| [0] | Single element |
| [1,1,1,1] | All elements equal |
| [5,4, |