2422. Merge Operations to Turn Array Into a Palindrome π
Problem Description
You have an array nums
containing positive integers. Your goal is to transform this array into a palindrome using the minimum number of operations possible.
The operation you can perform is:
- Select any two adjacent elements in the array and replace them with their sum
For example, if nums = [1, 2, 3, 1]
, you can combine the elements at positions 1 and 2 (values 2 and 3) to get [1, 5, 1]
.
A palindrome array reads the same forwards and backwards. For instance, [1, 5, 1]
is a palindrome because the first element equals the last element.
The solution uses a two-pointer greedy approach:
- Start with pointers at both ends of the array (
i
at the beginning,j
at the end) - Keep track of the current values at these positions using variables
a
andb
- When
a < b
: Merge elements on the left side by movingi
forward and adding the next element toa
- When
a > b
: Merge elements on the right side by movingj
backward and adding the previous element tob
- When
a = b
: These positions match (as required for a palindrome), so move both pointers inward and reseta
andb
to the new elements - Continue until the pointers meet or cross
Each merge operation (when a β b
) counts as one operation. The algorithm ensures we create matching values at symmetric positions with the minimum number of merges needed.
Intuition
To make an array a palindrome, we need the first element to equal the last element, the second element to equal the second-to-last element, and so on. Since we can only merge adjacent elements, we need to think about how to achieve these matching pairs efficiently.
The key insight is that we can work from both ends of the array simultaneously. Imagine comparing the leftmost and rightmost elements. If they're already equal, great - they satisfy the palindrome condition, so we can move on to the next pair inward. But if they're not equal, we need to make them equal somehow.
Since we can only merge adjacent elements, we have two choices when the ends don't match:
- Merge elements on the left side to increase the left value
- Merge elements on the right side to increase the right value
But which side should we merge? Here's the clever part: we should always merge on the side with the smaller value. Why? Because we want to make the two ends equal with the minimum number of operations. If a < b
, merging on the left brings a
closer to b
. If b < a
, merging on the right brings b
closer to a
.
This greedy strategy works because:
- We're building towards equality from both ends
- Each merge operation increases the value on that side
- By always merging the smaller side, we're taking the most direct path to making the values equal
- Once the values match, those positions are "locked in" for the palindrome, and we can move inward to handle the next pair
The process naturally terminates when our pointers meet or cross in the middle, at which point we've successfully created all the necessary matching pairs for a palindrome.
Learn more about Greedy and Two Pointers patterns.
Solution Approach
The implementation uses a greedy two-pointer approach to build the palindrome from outside to inside.
Initialize the pointers and tracking variables:
- Set
i = 0
andj = len(nums) - 1
to point to the first and last positions - Initialize
a = nums[i]
andb = nums[j]
to track the current values at these positions - Set
ans = 0
to count the number of operations
Main processing loop (while i < j
):
The algorithm compares the values a
and b
and takes different actions based on their relationship:
-
When
a < b
:- Move the left pointer forward:
i += 1
- Merge the new element into the left sum:
a += nums[i]
- Increment the operation count:
ans += 1
- This effectively combines
nums[i-1]
andnums[i]
into a single value
- Move the left pointer forward:
-
When
b < a
:- Move the right pointer backward:
j -= 1
- Merge the new element into the right sum:
b += nums[j]
- Increment the operation count:
ans += 1
- This effectively combines
nums[j]
andnums[j+1]
into a single value
- Move the right pointer backward:
-
When
a == b
:- The current positions have matching values (satisfying palindrome requirement)
- Move both pointers inward:
i += 1
andj -= 1
- Reset to the new elements:
a = nums[i]
andb = nums[j]
- No operation needed since these positions already match
Termination:
The loop continues until i >= j
, meaning we've processed all positions that need to match for the palindrome. At this point, return ans
as the minimum number of operations required.
The algorithm correctly handles all cases:
- If the array is already a palindrome, it will move through without any operations
- If merging is needed, it always chooses the optimal side (smaller value) to minimize operations
- The greedy choice at each step leads to the global minimum
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 algorithm with nums = [1, 2, 3, 1]
:
Initial Setup:
i = 0
,j = 3
a = nums[0] = 1
,b = nums[3] = 1
ans = 0
Step 1: Compare a = 1
and b = 1
- Since
a == b
, the first and last positions already match for our palindrome - Move both pointers inward:
i = 1
,j = 2
- Reset values:
a = nums[1] = 2
,b = nums[2] = 3
- No operation needed
Step 2: Compare a = 2
and b = 3
- Since
a < b
, we need to merge on the left side - Move left pointer:
i = 2
- Add to left sum:
a = 2 + nums[2] = 2 + 3 = 5
- Increment operations:
ans = 1
Step 3: Check loop condition
- Now
i = 2
andj = 2
, soi >= j
- Loop terminates
Result: The algorithm returns ans = 1
The transformed array would conceptually be [1, 5, 1]
(merging positions 1 and 2), which is indeed a palindrome achieved with the minimum 1 operation.
Let's verify with another example: nums = [4, 2, 1, 3, 5]
:
Initial: i = 0
, j = 4
, a = 4
, b = 5
, ans = 0
Step 1: a = 4 < b = 5
- Merge left:
i = 1
,a = 4 + 2 = 6
,ans = 1
Step 2: a = 6 > b = 5
- Merge right:
j = 3
,b = 5 + 3 = 8
,ans = 2
Step 3: a = 6 < b = 8
- Merge left:
i = 2
,a = 6 + 1 = 7
,ans = 3
Step 4: a = 7 < b = 8
- Merge left:
i = 3
,a = 7 + 3 = 10
,ans = 4
Step 5: i = 3 >= j = 3
, loop ends
Result: Returns ans = 4
operations to create palindrome [10, 10]
Solution Implementation
1class Solution:
2 def minimumOperations(self, nums: List[int]) -> int:
3 # Initialize two pointers at the start and end of the array
4 left_ptr, right_ptr = 0, len(nums) - 1
5
6 # Track cumulative sums from left and right sides
7 left_sum = nums[left_ptr]
8 right_sum = nums[right_ptr]
9
10 # Count the number of merge operations performed
11 operation_count = 0
12
13 # Process array from both ends until pointers meet
14 while left_ptr < right_ptr:
15 if left_sum < right_sum:
16 # Left sum is smaller, merge next element from left
17 left_ptr += 1
18 left_sum += nums[left_ptr]
19 operation_count += 1
20 elif right_sum < left_sum:
21 # Right sum is smaller, merge next element from right
22 right_ptr -= 1
23 right_sum += nums[right_ptr]
24 operation_count += 1
25 else:
26 # Both sums are equal, move both pointers inward
27 # and reset sums to new elements
28 left_ptr += 1
29 right_ptr -= 1
30 if left_ptr <= right_ptr:
31 left_sum = nums[left_ptr]
32 right_sum = nums[right_ptr]
33
34 return operation_count
35
1class Solution {
2 public int minimumOperations(int[] nums) {
3 // Initialize two pointers at the start and end of the array
4 int left = 0;
5 int right = nums.length - 1;
6
7 // Track cumulative sums from both ends
8 long leftSum = nums[left];
9 long rightSum = nums[right];
10
11 // Counter for the number of operations performed
12 int operationCount = 0;
13
14 // Process the array from both ends until pointers meet
15 while (left < right) {
16 if (leftSum < rightSum) {
17 // Left sum is smaller, merge with next element from left
18 left++;
19 leftSum += nums[left];
20 operationCount++;
21 } else if (rightSum < leftSum) {
22 // Right sum is smaller, merge with next element from right
23 right--;
24 rightSum += nums[right];
25 operationCount++;
26 } else {
27 // Both sums are equal, move both pointers without merging
28 left++;
29 right--;
30 leftSum = nums[left];
31 rightSum = nums[right];
32 }
33 }
34
35 return operationCount;
36 }
37}
38
1class Solution {
2public:
3 int minimumOperations(vector<int>& nums) {
4 // Initialize two pointers: left and right
5 int left = 0;
6 int right = nums.size() - 1;
7
8 // Track cumulative sums from both ends
9 long leftSum = nums[left];
10 long rightSum = nums[right];
11
12 // Count the number of operations performed
13 int operationCount = 0;
14
15 // Process array from both ends until pointers meet
16 while (left < right) {
17 if (leftSum < rightSum) {
18 // Left sum is smaller, merge with next element from left
19 left++;
20 leftSum += nums[left];
21 operationCount++;
22 } else if (rightSum < leftSum) {
23 // Right sum is smaller, merge with next element from right
24 right--;
25 rightSum += nums[right];
26 operationCount++;
27 } else {
28 // Both sums are equal, move both pointers without merging
29 left++;
30 right--;
31 leftSum = nums[left];
32 rightSum = nums[right];
33 }
34 }
35
36 return operationCount;
37 }
38};
39
1function minimumOperations(nums: number[]): number {
2 // Initialize two pointers: left and right
3 let left: number = 0;
4 let right: number = nums.length - 1;
5
6 // Track cumulative sums from both ends
7 let leftSum: number = nums[left];
8 let rightSum: number = nums[right];
9
10 // Count the number of operations performed
11 let operationCount: number = 0;
12
13 // Process array from both ends until pointers meet
14 while (left < right) {
15 if (leftSum < rightSum) {
16 // Left sum is smaller, merge with next element from left
17 left++;
18 leftSum += nums[left];
19 operationCount++;
20 } else if (rightSum < leftSum) {
21 // Right sum is smaller, merge with next element from right
22 right--;
23 rightSum += nums[right];
24 operationCount++;
25 } else {
26 // Both sums are equal, move both pointers without merging
27 left++;
28 right--;
29 // Only update sums if pointers haven't crossed
30 if (left <= right) {
31 leftSum = nums[left];
32 rightSum = nums[right];
33 }
34 }
35 }
36
37 return operationCount;
38}
39
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the array nums
.
The algorithm uses two pointers i
and j
that start from opposite ends of the array and move towards each other. In each iteration of the while loop, at least one pointer moves closer to the other (either i
increases, j
decreases, or both move when a == b
). Since the pointers start n-1
positions apart and the loop continues while i < j
, each element in the array is visited at most once. Therefore, the maximum number of iterations is n-1
, resulting in linear time complexity.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space regardless of the input size. The variables used are:
- Two pointers
i
andj
- Two accumulator variables
a
andb
- One counter variable
ans
All these variables occupy constant space that doesn't scale with the input size n
, hence the space complexity is constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Reset Logic When Sums Are Equal
A critical pitfall occurs when left_sum == right_sum
and we move both pointers inward. The code must check if valid elements still exist before resetting the sums.
Problematic Code:
else: # left_sum == right_sum left_ptr += 1 right_ptr -= 1 left_sum = nums[left_ptr] # Can cause IndexError! right_sum = nums[right_ptr]
Issue: When the pointers cross (left_ptr > right_ptr
), accessing nums[left_ptr]
or nums[right_ptr]
will either access wrong indices or cause an IndexError.
Solution: Add a boundary check before resetting:
else: # left_sum == right_sum left_ptr += 1 right_ptr -= 1 if left_ptr < right_ptr: left_sum = nums[left_ptr] right_sum = nums[right_ptr]
2. Off-by-One Error in Loop Condition
Using while left_ptr <= right_ptr
instead of while left_ptr < right_ptr
can lead to unnecessary iterations or incorrect results.
Why it matters: When left_ptr == right_ptr
, we're at the middle element (for odd-length arrays), which doesn't need a matching pair. The palindrome property is already satisfied for this central element.
3. Not Handling Single Element or Two Element Arrays
Edge Cases to Consider:
nums = [5]
- Already a palindrome, should return 0nums = [1, 2]
- Needs 1 operation to become[3]
Solution: The algorithm naturally handles these cases, but ensure your implementation doesn't have special conditions that break for small inputs.
4. Forgetting to Track Cumulative Sums
A common mistake is resetting the sum completely when merging instead of accumulating:
Wrong:
if left_sum < right_sum: left_ptr += 1 left_sum = nums[left_ptr] # Wrong! Should accumulate operation_count += 1
Correct:
if left_sum < right_sum: left_ptr += 1 left_sum += nums[left_ptr] # Accumulate the sum operation_count += 1
5. Misunderstanding the Merge Operation
The operation replaces two adjacent elements with their sum, reducing the array length by 1. Some might mistakenly think:
- The operation swaps elements
- The operation can be applied to non-adjacent elements
- The merged value replaces both positions
Clarification: Each merge combines two adjacent elements into one, effectively removing one element from the array.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted 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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!