2263. Make Array Non-decreasing or Non-increasing 🔒
Problem Description
You have an array of integers nums
indexed from 0. You can perform operations on this array where each operation allows you to:
- Select any index
i
(where0 <= i < nums.length
) - Either increase
nums[i]
by 1 or decreasenums[i]
by 1
Your goal is to transform the array so that it becomes either:
- Non-decreasing: Each element is less than or equal to the next element (
nums[i] <= nums[i+1]
for all valid i) - Non-increasing: Each element is greater than or equal to the next element (
nums[i] >= nums[i+1]
for all valid i)
You need to find the minimum total number of operations required to achieve either of these two conditions.
For example:
- If
nums = [3, 2, 4, 1]
, you could make it non-decreasing by changing it to something like[2, 2, 2, 2]
or[1, 1, 4, 4]
- Or you could make it non-increasing by changing it to something like
[4, 4, 1, 1]
or[3, 2, 1, 1]
- The answer would be the minimum operations needed among all possible transformations
The solution uses dynamic programming to find the minimum cost to make the array non-decreasing, then checks the reverse array to find the minimum cost for non-increasing, and returns the smaller of the two values.
Intuition
The key insight is recognizing that we need to find the optimal target values for each position in the array. When making an array non-decreasing, each element can be adjusted to any value, but there's a constraint: each element must be less than or equal to the next one.
First, let's think about the range of possible values. Since we can only increment or decrement by 1 in each operation, and we want to minimize operations, the optimal target values should be close to the original values. In fact, the optimal solution will always have each final value be one of the original values in the array (or within the range of original values). This is because moving to values outside this range would just add unnecessary operations.
Since the problem constraints limit values to a reasonable range (0-1000 based on the code), we can use dynamic programming where we consider all possible values for each position.
For a non-decreasing array, we define f[i][j]
as the minimum operations needed to make the first i
elements non-decreasing, where the i-th
element becomes value j
. The transition is: for position i
, if we want it to have value j
, we need to consider all values k <= j
for position i-1
. The cost is the minimum cost to reach value k
at position i-1
, plus the cost to change element i
from its original value to j
(which is abs(nums[i] - j)
).
The optimization in the code uses the fact that for non-decreasing arrays, if we fix the value at position i
to be j
, we want the minimum cost among all valid previous states where the value was <= j
. We can maintain this minimum as we iterate through possible values.
For non-increasing arrays, we can reuse the same logic by simply reversing the array. A non-increasing array reversed becomes a non-decreasing array, so we solve the same problem on nums[::-1]
.
The final answer is the minimum between making the array non-decreasing and making it non-increasing.
Learn more about Greedy and Dynamic Programming patterns.
Solution Approach
The solution implements a dynamic programming approach with space optimization. Let's walk through the implementation step by step:
1. Main Function Structure:
The solution defines a helper function solve(nums)
that finds the minimum operations to make the array non-decreasing. It then calls this function twice - once with the original array and once with the reversed array - and returns the minimum of the two results.
2. Dynamic Programming Setup:
n = len(nums)
f = [[0] * 1001 for _ in range(n + 1)]
We create a 2D DP table f
where f[i][j]
represents the minimum operations needed to make the first i
elements non-decreasing, with the i-th
element having value j
. The table has dimensions (n+1) × 1001
to handle all possible values from 0 to 1000.
3. DP Transition:
for i, x in enumerate(nums, 1):
mi = inf
for j in range(1001):
if mi > f[i - 1][j]:
mi = f[i - 1][j]
f[i][j] = mi + abs(x - j)
For each element at position i
(1-indexed) with original value x
:
- We iterate through all possible target values
j
from 0 to 1000 - We maintain
mi
as the minimum cost to reach positioni-1
with any value<= j
- As we iterate through
j
, we updatemi
whenever we find a smaller cost atf[i-1][j]
- The cost for position
i
to have valuej
is:mi + abs(x - j)
mi
: minimum cost to make the firsti-1
elements non-decreasing with the last element<= j
abs(x - j)
: cost to change the current element fromx
toj
4. Key Optimization:
The clever part is maintaining the running minimum mi
as we iterate through values. Since we want a non-decreasing array, if element i
has value j
, then element i-1
can have any value from 0 to j
. Instead of checking all these values for each j
, we maintain the minimum incrementally.
5. Final Result:
return min(f[n])
After processing all elements, f[n][j]
contains the minimum operations to make the entire array non-decreasing with the last element being j
. We return the minimum among all possible final values.
6. Handling Both Cases:
return min(solve(nums), solve(nums[::-1]))
To handle both non-decreasing and non-increasing cases:
solve(nums)
finds the cost for non-decreasingsolve(nums[::-1])
finds the cost for non-increasing (by reversing the array)- We return the minimum of the two
The time complexity is O(n × m)
where n
is the array length and m
is the range of values (1001), and the space complexity is also O(n × m)
.
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 = [3, 2, 4, 1]
.
Finding minimum operations for non-decreasing:
We'll build a DP table where f[i][j]
= minimum operations to make first i
elements non-decreasing with element i
having value j
.
For element 1 (value=3):
f[1][0] = |3-0| = 3
(change 3 to 0)f[1][1] = |3-1| = 2
(change 3 to 1)f[1][2] = |3-2| = 1
(change 3 to 2)f[1][3] = |3-3| = 0
(keep 3 as 3)f[1][4] = |3-4| = 1
(change 3 to 4)
For element 2 (value=2):
- To make element 2 have value
j
, element 1 must be ≤j
- For
j=2
: minimum off[1][0]
,f[1][1]
,f[1][2]
= min(3,2,1) = 1f[2][2] = 1 + |2-2| = 1
- For
j=3
: minimum off[1][0]
,f[1][1]
,f[1][2]
,f[1][3]
= min(3,2,1,0) = 0f[2][3] = 0 + |2-3| = 1
- For
j=4
: minimum includes all previous = 0f[2][4] = 0 + |2-4| = 2
For element 3 (value=4):
- For
j=4
: minimum of allf[2][k]
wherek≤4
- Best previous state is
f[2][2]=1
orf[2][3]=1
f[3][4] = 1 + |4-4| = 1
- Best previous state is
For element 4 (value=1):
- For
j=4
: minimum of allf[3][k]
wherek≤4
= 1f[4][4] = 1 + |1-4| = 4
- For
j=1
: minimum off[3][0]
,f[3][1]
(higher costs) - For
j=2
: minimum includesf[3][2]
= 3f[4][2] = 3 + |1-2| = 4
Minimum for non-decreasing: min(f[4]) = 4 (achieved when final array is [2,2,2,2] or similar)
Finding minimum operations for non-increasing:
Reverse the array: [1, 4, 2, 3]
and apply the same algorithm.
After similar calculations, we'd find that making [1,4,2,3] non-decreasing (which corresponds to making [3,2,4,1] non-increasing) requires 3 operations.
One optimal non-increasing result would be [3,2,1,1] with operations:
- Keep 3 as 3 (0 ops)
- Keep 2 as 2 (0 ops)
- Change 4 to 1 (3 ops)
- Keep 1 as 1 (0 ops)
- Total: 3 operations
Final answer: min(4, 3) = 3
Solution Implementation
1class Solution:
2 def convertArray(self, nums: List[int]) -> int:
3 def solve(nums):
4 """
5 Calculate minimum operations to make array non-decreasing.
6 Uses dynamic programming where dp[i][j] represents the minimum cost
7 to make the first i elements non-decreasing with the i-th element being j.
8 """
9 n = len(nums)
10 # dp[i][j] = minimum cost to make first i elements non-decreasing
11 # with the i-th element having value j
12 dp = [[0] * 1001 for _ in range(n + 1)]
13
14 # Process each element in the array
15 for i, current_value in enumerate(nums, 1):
16 min_cost = float('inf')
17
18 # Try all possible values for the current position (0 to 1000)
19 for target_value in range(1001):
20 # Update minimum cost from previous row
21 # This ensures non-decreasing property
22 if min_cost > dp[i - 1][target_value]:
23 min_cost = dp[i - 1][target_value]
24
25 # Cost to change current element to target_value
26 # plus the minimum cost to reach this state
27 dp[i][target_value] = min_cost + abs(current_value - target_value)
28
29 # Return the minimum cost among all possible final values
30 return min(dp[n])
31
32 # Try both non-decreasing and non-increasing transformations
33 # Non-increasing is achieved by reversing the array and finding non-decreasing
34 return min(solve(nums), solve(nums[::-1]))
35
1class Solution {
2 /**
3 * Finds the minimum number of operations to make the array either non-decreasing or non-increasing.
4 * Each operation changes an element to any value, with cost equal to the absolute difference.
5 *
6 * @param nums the input array
7 * @return minimum cost to make array monotonic
8 */
9 public int convertArray(int[] nums) {
10 // Try both non-decreasing and non-increasing (by reversing and solving)
11 int nonDecreasingCost = solve(nums);
12 int nonIncreasingCost = solve(reverse(nums));
13 return Math.min(nonDecreasingCost, nonIncreasingCost);
14 }
15
16 /**
17 * Calculates minimum cost to make array non-decreasing using dynamic programming.
18 *
19 * @param nums the input array
20 * @return minimum cost to make array non-decreasing
21 */
22 private int solve(int[] nums) {
23 int length = nums.length;
24 // dp[i][j] represents minimum cost to make first i elements non-decreasing
25 // with the i-th element having value j
26 int[][] dp = new int[length + 1][1001];
27
28 // Process each position in the array
29 for (int position = 1; position <= length; ++position) {
30 int minPreviousCost = Integer.MAX_VALUE;
31
32 // Try all possible values from 0 to 1000 for current position
33 for (int currentValue = 0; currentValue <= 1000; ++currentValue) {
34 // Find minimum cost from previous position with value <= currentValue
35 minPreviousCost = Math.min(minPreviousCost, dp[position - 1][currentValue]);
36
37 // Calculate cost for setting current element to currentValue
38 int changeCost = Math.abs(currentValue - nums[position - 1]);
39 dp[position][currentValue] = minPreviousCost + changeCost;
40 }
41 }
42
43 // Find minimum cost among all possible values for the last element
44 int minimumTotalCost = Integer.MAX_VALUE;
45 for (int finalValue : dp[length]) {
46 minimumTotalCost = Math.min(minimumTotalCost, finalValue);
47 }
48
49 return minimumTotalCost;
50 }
51
52 /**
53 * Reverses the array in place.
54 *
55 * @param nums the array to reverse
56 * @return the reversed array (same reference)
57 */
58 private int[] reverse(int[] nums) {
59 int left = 0;
60 int right = nums.length - 1;
61
62 while (left < right) {
63 // Swap elements at left and right positions
64 int temp = nums[left];
65 nums[left] = nums[right];
66 nums[right] = temp;
67
68 left++;
69 right--;
70 }
71
72 return nums;
73 }
74}
75
1class Solution {
2public:
3 int convertArray(vector<int>& nums) {
4 // Try making array non-decreasing
5 int nonDecreasingCost = calculateMinCost(nums);
6
7 // Try making array non-increasing (by reversing and making it non-decreasing)
8 reverse(nums.begin(), nums.end());
9 int nonIncreasingCost = calculateMinCost(nums);
10
11 // Return the minimum cost between the two options
12 return min(nonDecreasingCost, nonIncreasingCost);
13 }
14
15private:
16 int calculateMinCost(vector<int>& nums) {
17 int n = nums.size();
18
19 // dp[i][j] represents the minimum cost to make first i elements non-decreasing
20 // where the i-th element has value j (j ranges from 0 to 1000)
21 int dp[n + 1][1001];
22 memset(dp, 0, sizeof(dp));
23
24 // Process each element in the array
25 for (int i = 1; i <= n; ++i) {
26 int minPrevCost = 1 << 30; // Initialize with a large value
27
28 // Try all possible values for current position (0 to 1000)
29 for (int j = 0; j <= 1000; ++j) {
30 // Find minimum cost from previous position for values <= j
31 // This ensures non-decreasing property
32 minPrevCost = min(minPrevCost, dp[i - 1][j]);
33
34 // Calculate cost for setting current element to value j
35 // Cost = minimum previous cost + cost of changing nums[i-1] to j
36 dp[i][j] = minPrevCost + abs(nums[i - 1] - j);
37 }
38 }
39
40 // Find the minimum cost among all possible values for the last element
41 return *min_element(dp[n], dp[n] + 1001);
42 }
43};
44
1function convertArray(nums: number[]): number {
2 // Try making array non-decreasing
3 const nonDecreasingCost = calculateMinCost(nums);
4
5 // Try making array non-increasing (by reversing and making it non-decreasing)
6 nums.reverse();
7 const nonIncreasingCost = calculateMinCost(nums);
8
9 // Return the minimum cost between the two options
10 return Math.min(nonDecreasingCost, nonIncreasingCost);
11}
12
13function calculateMinCost(nums: number[]): number {
14 const n = nums.length;
15
16 // dp[i][j] represents the minimum cost to make first i elements non-decreasing
17 // where the i-th element has value j (j ranges from 0 to 1000)
18 const dp: number[][] = Array(n + 1).fill(null).map(() => Array(1001).fill(0));
19
20 // Process each element in the array
21 for (let i = 1; i <= n; i++) {
22 let minPrevCost = 1 << 30; // Initialize with a large value
23
24 // Try all possible values for current position (0 to 1000)
25 for (let j = 0; j <= 1000; j++) {
26 // Find minimum cost from previous position for values <= j
27 // This ensures non-decreasing property
28 minPrevCost = Math.min(minPrevCost, dp[i - 1][j]);
29
30 // Calculate cost for setting current element to value j
31 // Cost = minimum previous cost + cost of changing nums[i-1] to j
32 dp[i][j] = minPrevCost + Math.abs(nums[i - 1] - j);
33 }
34 }
35
36 // Find the minimum cost among all possible values for the last element
37 return Math.min(...dp[n]);
38}
39
Time and Space Complexity
Time Complexity: O(n * m)
where n
is the length of the input array and m = 1001
(the range of possible values).
The algorithm calls the solve
function twice - once with the original array and once with the reversed array. Inside solve
:
- There are two nested loops: the outer loop iterates through
n
elements of the array - The inner loop iterates through 1001 possible values (0 to 1000)
- For each combination of
i
andj
, we perform constant time operations (comparison, assignment, and absolute value calculation) - Finding the minimum in
f[i-1][j]
is done incrementally with themi
variable, so it doesn't add extra complexity - The final
min(f[n])
operation takesO(m)
time
Total time: 2 * O(n * m) = O(n * m) = O(1001n) = O(n)
Space Complexity: O(n * m)
where n
is the length of the input array and m = 1001
.
The space is dominated by:
- The 2D DP array
f
with dimensions(n+1) × 1001
, requiringO(n * m)
space - The reversed array
nums[::-1]
creates a copy of the input array, requiringO(n)
space - Other variables like
mi
,i
,j
,x
use constant space
Total space: O(n * m) + O(n) = O(n * m) = O(1001n) = O(n)
Note: Since m = 1001
is a constant, both complexities can be simplified to O(n)
in terms of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Range Assumption
One of the most common pitfalls is assuming the wrong range for array values. The solution uses [0, 1000]
as the range, but if the input contains negative numbers or values greater than 1000, the solution will fail.
Example of the problem:
nums = [-5, 10, 2000] # Contains negative and values > 1000 # The current solution will crash or give incorrect results
Solution: Dynamically determine the range from the input array:
def solve(nums):
n = len(nums)
min_val = min(nums)
max_val = max(nums)
# Create DP table with actual range
dp = [[0] * (max_val - min_val + 1) for _ in range(n + 1)]
for i, current_value in enumerate(nums, 1):
min_cost = float('inf')
# Iterate through the actual range
for target_value in range(min_val, max_val + 1):
idx = target_value - min_val # Adjust index for DP table
if min_cost > dp[i - 1][idx]:
min_cost = dp[i - 1][idx]
dp[i][idx] = min_cost + abs(current_value - target_value)
return min(dp[n])
2. Memory Overflow with Large Ranges
If the array contains values with a very large range (e.g., [-10^9, 10^9]
), creating a DP table for all possible values will cause memory issues.
Solution: Use coordinate compression - only consider values that actually appear in the array:
def solve(nums):
n = len(nums)
# Only consider values that appear in the array
unique_values = sorted(set(nums))
m = len(unique_values)
# Map values to indices
value_to_idx = {v: i for i, v in enumerate(unique_values)}
dp = [[0] * m for _ in range(n + 1)]
for i, current_value in enumerate(nums, 1):
min_cost = float('inf')
for j, target_value in enumerate(unique_values):
if min_cost > dp[i - 1][j]:
min_cost = dp[i - 1][j]
dp[i][j] = min_cost + abs(current_value - target_value)
return min(dp[n])
3. Space Optimization Opportunity Missed
The current solution uses O(n × m) space, but since we only need the previous row to compute the current row, we can optimize to O(m) space.
Solution: Use rolling arrays:
def solve(nums):
n = len(nums)
min_val, max_val = min(nums), max(nums)
range_size = max_val - min_val + 1
# Use only two rows instead of n+1 rows
prev = [0] * range_size
curr = [0] * range_size
for current_value in nums:
min_cost = float('inf')
for target_value in range(min_val, max_val + 1):
idx = target_value - min_val
if min_cost > prev[idx]:
min_cost = prev[idx]
curr[idx] = min_cost + abs(current_value - target_value)
prev, curr = curr, prev
return min(prev)
4. Not Handling Edge Cases
The solution might not properly handle edge cases like empty arrays or single-element arrays.
Solution: Add edge case checks:
def convertArray(self, nums: List[int]) -> int:
# Handle edge cases
if not nums or len(nums) <= 1:
return 0
def solve(nums):
# ... rest of the implementation
return min(solve(nums), solve(nums[::-1]))
Depth first search is equivalent to which of the tree traversal order?
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!