LeetCode 1144 - Decrease Elements To Make Array Zigzag
The problem gives an integer array nums and allows only one type of operation, decreasing any element by 1. We may perform this operation as many times as needed on any elements. Our goal is to transform the array into a zigzag array with the minimum number of moves.
Difficulty: 🟡 Medium
Topics: Array, Greedy
Solution
Problem Understanding
The problem gives an integer array nums and allows only one type of operation, decreasing any element by 1. We may perform this operation as many times as needed on any elements.
Our goal is to transform the array into a zigzag array with the minimum number of moves.
A zigzag array can follow one of two valid patterns:
- Even indices are peaks:
nums[0] > nums[1] < nums[2] > nums[3] < nums[4] ...
2. Odd indices are peaks:
nums[0] < nums[1] > nums[2] < nums[3] > nums[4] ...
We are free to choose whichever pattern requires fewer total decreases.
The important detail is that we are only allowed to decrease values. Increasing an element is never allowed. This restriction completely changes the strategy because if an element is too small relative to its neighbors, we cannot raise it, we must instead reduce surrounding values.
The input size is relatively small:
1 <= nums.length <= 10001 <= nums[i] <= 1000
These constraints tell us that even an O(n^2) solution would likely pass, but we should still aim for a clean linear solution.
A key observation is that every position only depends on its immediate neighbors. Whether an element satisfies the zigzag requirement can be determined locally by comparing it with adjacent values.
Several edge cases are important:
- Arrays of length
1are already zigzag arrays because there are no adjacent elements to violate the pattern. - Arrays with equal neighboring values require reductions because strict inequalities are required.
- Boundary elements only have one neighbor, so comparisons must handle missing left or right neighbors carefully.
- Large values are harmless because we only perform arithmetic differences.
Approaches
Brute Force Approach
A brute force strategy would attempt to simulate both zigzag patterns by repeatedly adjusting elements until all inequalities are satisfied.
For example, we could repeatedly scan the array and whenever a position violates the zigzag condition, decrease the offending element until the condition becomes valid. We would do this separately for:
- even indices as peaks
- odd indices as peaks
This eventually produces a valid zigzag array because decreasing values can always fix violations.
However, this approach is inefficient because repeatedly decrementing elements one step at a time may require many operations. Since values can be as large as 1000, the simulation may perform unnecessary repeated work.
The key inefficiency is that we do not actually need to simulate each decrement individually. We can directly compute how many decreases are required for each position.
Optimal Greedy Approach
The important insight is that each position can be handled independently once we decide which parity should contain the valleys.
Suppose we want index i to be a valley. Then it must satisfy:
nums[i] < left neighbor
nums[i] < right neighbor
Because we are only allowed to decrease values, the only way to guarantee this is to reduce nums[i] itself.
To make nums[i] strictly smaller than both neighbors, its maximum allowed value becomes:
min(left neighbor, right neighbor) - 1
If nums[i] is already smaller than both neighbors, no work is needed.
Otherwise, the required decreases are:
nums[i] - (minNeighbor - 1)
We compute this independently for every valley position.
We evaluate two scenarios:
- Even indices are valleys
- Odd indices are valleys
The minimum total cost of the two scenarios is the answer.
This works because each valley adjustment is local and does not interfere with other valleys.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * max(nums)) | O(1) | Simulates decrements repeatedly |
| Optimal | O(n) | O(1) | Directly computes required reductions |
Algorithm Walkthrough
- Create a helper function that computes the total moves needed when a chosen parity represents valley positions.
- For every index
iwith the selected parity:
- Find the left neighbor if it exists.
- Find the right neighbor if it exists.
- Compute the minimum neighbor value.
- Since the current element must become strictly smaller than both neighbors, the largest valid value is:
minNeighbor - 1
- If
nums[i]is already smaller than both neighbors, no moves are required. - Otherwise, calculate how many decrements are needed:
nums[i] - (minNeighbor - 1)
- Add this value to the running total.
- Repeat for all valley positions.
- Run the helper twice:
- once with even indices as valleys
- once with odd indices as valleys
- Return the smaller result.
Why it works
For any valid zigzag configuration, one parity must contain all valleys. A valley only needs to be smaller than its adjacent neighbors, so its required adjustment depends solely on local comparisons.
Because decreasing one valley does not invalidate another valley requirement, each position can be optimized independently. Computing the minimum necessary reduction for every valley therefore produces the globally minimum number of moves.
Python Solution
from typing import List
class Solution:
def movesToMakeZigzag(self, nums: List[int]) -> int:
n = len(nums)
def calculate_moves(valley_parity: int) -> int:
moves = 0
for i in range(valley_parity, n, 2):
left = nums[i - 1] if i > 0 else float('inf')
right = nums[i + 1] if i + 1 < n else float('inf')
min_neighbor = min(left, right)
needed = max(0, nums[i] - (min_neighbor - 1))
moves += needed
return moves
return min(calculate_moves(0), calculate_moves(1))
The implementation follows the greedy observation directly.
The helper function calculate_moves evaluates one zigzag configuration. The parameter valley_parity determines which indices should become valleys.
For each valley position, the code retrieves the left and right neighbors. Missing neighbors are treated as positive infinity so boundary elements only compare against the neighbor that actually exists.
The smallest neighboring value determines the maximum legal value for the current element. If the current element is already valid, max(0, ...) ensures no unnecessary moves are added.
Finally, the algorithm computes the total cost for both possible zigzag patterns and returns the smaller one.
Go Solution
func movesToMakeZigzag(nums []int) int {
n := len(nums)
calculateMoves := func(valleyParity int) int {
moves := 0
for i := valleyParity; i < n; i += 2 {
left := 1 << 30
right := 1 << 30
if i > 0 {
left = nums[i-1]
}
if i+1 < n {
right = nums[i+1]
}
minNeighbor := left
if right < minNeighbor {
minNeighbor = right
}
needed := nums[i] - (minNeighbor - 1)
if needed > 0 {
moves += needed
}
}
return moves
}
evenValleys := calculateMoves(0)
oddValleys := calculateMoves(1)
if evenValleys < oddValleys {
return evenValleys
}
return oddValleys
}
The Go implementation mirrors the Python logic closely.
Since Go does not have a built in infinity value for integers, a very large number (1 << 30) is used for missing neighbors.
The rest of the logic remains identical:
- evaluate both parity configurations
- compute required reductions locally
- return the minimum total moves
Integer overflow is not a concern because all values remain small under the problem constraints.
Worked Examples
Example 1
Input:
nums = [1, 2, 3]
We test both zigzag patterns.
Case 1: Even indices are valleys
Target pattern:
1 < 2 > 3
Valley indices: 0, 2
| Index | Value | Neighbors | Min Neighbor | Moves Needed |
|---|---|---|---|---|
| 0 | 1 | 2 | 2 | 0 |
| 2 | 3 | 2 | 2 | 2 |
Total moves = 2
After decreasing:
[1, 2, 1]
Case 2: Odd indices are valleys
Target pattern:
1 > 2 < 3
Valley indices: 1
| Index | Value | Neighbors | Min Neighbor | Moves Needed |
|---|---|---|---|---|
| 1 | 2 | 1, 3 | 1 | 2 |
Total moves = 2
Minimum answer:
2
Example 2
Input:
nums = [9, 6, 1, 6, 2]
Case 1: Even indices are valleys
Valley indices: 0, 2, 4
| Index | Value | Neighbors | Min Neighbor | Moves Needed |
|---|---|---|---|---|
| 0 | 9 | 6 | 6 | 4 |
| 2 | 1 | 6, 6 | 6 | 0 |
| 4 | 2 | 6 | 6 | 0 |
Total = 4
Case 2: Odd indices are valleys
Valley indices: 1, 3
| Index | Value | Neighbors | Min Neighbor | Moves Needed |
|---|---|---|---|---|
| 1 | 6 | 9, 1 | 1 | 6 |
| 3 | 6 | 1, 2 | 1 | 6 |
Total = 12
Minimum answer:
4
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is processed at most twice |
| Space | O(1) | Only constant extra variables are used |
The algorithm performs two linear scans of the array, one for each zigzag pattern. Each scan processes every other element and performs only constant time operations.
No auxiliary data structures proportional to input size are required, so the space usage remains constant.
Test Cases
from typing import List
class Solution:
def movesToMakeZigzag(self, nums: List[int]) -> int:
n = len(nums)
def calculate_moves(valley_parity: int) -> int:
moves = 0
for i in range(valley_parity, n, 2):
left = nums[i - 1] if i > 0 else float('inf')
right = nums[i + 1] if i + 1 < n else float('inf')
min_neighbor = min(left, right)
moves += max(0, nums[i] - (min_neighbor - 1))
return moves
return min(calculate_moves(0), calculate_moves(1))
sol = Solution()
assert sol.movesToMakeZigzag([1, 2, 3]) == 2 # provided example
assert sol.movesToMakeZigzag([9, 6, 1, 6, 2]) == 4 # provided example
assert sol.movesToMakeZigzag([1]) == 0 # single element already zigzag
assert sol.movesToMakeZigzag([2, 1]) == 0 # already zigzag
assert sol.movesToMakeZigzag([1, 2]) == 0 # already zigzag in opposite pattern
assert sol.movesToMakeZigzag([2, 2, 2]) == 2 # equal neighboring values
assert sol.movesToMakeZigzag([5, 5, 5, 5]) == 2 # repeated values
assert sol.movesToMakeZigzag([10, 10, 10]) == 1 # one reduction sufficient
assert sol.movesToMakeZigzag([3, 4, 5, 6]) == 4 # increasing sequence
assert sol.movesToMakeZigzag([6, 5, 4, 3]) == 4 # decreasing sequence
assert sol.movesToMakeZigzag([1, 3, 1, 3, 1]) == 0 # already zigzag
assert sol.movesToMakeZigzag([1000, 1, 1000]) == 0 # large values already valid
| Test | Why |
|---|---|
[1,2,3] |
Validates the basic example |
[9,6,1,6,2] |
Validates alternating reductions |
[1] |
Tests minimum array size |
[2,1] |
Tests already valid decreasing pair |
[1,2] |
Tests already valid increasing pair |
[2,2,2] |
Tests strict inequality handling |
[5,5,5,5] |
Tests repeated equal values |
[10,10,10] |
Tests minimal reduction computation |
[3,4,5,6] |
Tests monotonic increasing array |
[6,5,4,3] |
Tests monotonic decreasing array |
[1,3,1,3,1] |
Tests already zigzag pattern |
[1000,1,1000] |
Tests upper value range |
Edge Cases
One important edge case is an array with only one element. Since there are no adjacent elements, the array automatically satisfies both zigzag definitions. A careless implementation might attempt invalid neighbor access. This solution safely handles the case because missing neighbors are treated as infinity.
Another important case is arrays containing equal adjacent values, such as [2,2,2]. The zigzag requirement uses strict inequalities, so equality is not allowed. The implementation correctly computes how many decrements are needed to make one side strictly smaller by using minNeighbor - 1.
Boundary positions are also easy to mishandle because they only have one neighbor instead of two. For example, index 0 has no left neighbor and the last index has no right neighbor. The algorithm handles this cleanly by assigning nonexistent neighbors an infinite value, ensuring only real neighbors affect the reduction calculation.