Facebook Pixel

1187. Make Array Strictly Increasing

Problem Description

You are given two integer arrays arr1 and arr2. Your goal is to make arr1 strictly increasing using the minimum number of operations.

In each operation, you can:

  • Choose any index i from arr1 (where 0 <= i < arr1.length)
  • Choose any index j from arr2 (where 0 <= j < arr2.length)
  • Replace arr1[i] with arr2[j]

An array is strictly increasing if each element is strictly greater than the previous one (i.e., arr[i] < arr[i+1] for all valid i).

The function should return the minimum number of operations needed to make arr1 strictly increasing. If it's impossible to achieve this, return -1.

For example:

  • If arr1 = [1, 5, 3, 6, 7] and arr2 = [1, 3, 2, 4], you could replace arr1[2] (which is 3) with arr2[3] (which is 4) to get [1, 5, 4, 6, 7]. But this still isn't strictly increasing because 5 > 4. You'd need to make additional replacements.
  • The key challenge is finding the optimal sequence of replacements that minimizes the total number of operations while ensuring the final array is strictly increasing.
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to decide for each position whether to keep the original value or replace it with a value from arr2. This is a classic dynamic programming problem where we need to track the minimum operations needed to make the array strictly increasing up to each position.

First, let's think about what makes this problem tractable. If we're at position i and we want to keep arr1[i], we need the previous elements to be strictly less than arr1[i]. This means we might need to replace some previous elements to ensure this property holds.

The critical observation is that when we replace elements, we should replace them with the largest possible values that still maintain the strictly increasing property. Why? Because using larger values gives us more flexibility for future positions - it's easier to find values greater than what we've already placed.

This leads us to consider: for each position i, what if we keep arr1[i]? We need to look back and see how many consecutive elements before i we might need to replace. If we replace k consecutive elements before position i, we need to:

  1. Find k values from arr2 that form a strictly increasing sequence
  2. These values must be less than arr1[i]
  3. The element at position i-k-1 (if it exists) must be less than the first replacement value

To efficiently find replacement values, we sort arr2 and remove duplicates. When looking for replacements, we use binary search to find the largest possible values from arr2 that are still less than arr1[i].

The dynamic programming state f[i] represents the minimum operations needed to make arr1[0...i] strictly increasing while keeping arr1[i] unchanged. By adding sentinels -inf and inf at the beginning and end, we ensure that the first and last elements are never replaced, simplifying our logic. The answer will be f[n-1] since the last sentinel inf ensures any valid sequence before it will be strictly increasing.

Learn more about Binary Search, Dynamic Programming and Sorting patterns.

Solution Approach

The implementation follows these key steps:

1. Preprocessing arr2:

arr2.sort()
m = 0
for x in arr2:
    if m == 0 or x != arr2[m - 1]:
        arr2[m] = x
        m += 1
arr2 = arr2[:m]

We sort arr2 and remove duplicates to create a pool of unique replacement values in ascending order. This allows us to use binary search efficiently later.

2. Adding Sentinels:

arr = [-inf] + arr1 + [inf]
n = len(arr)

We add -inf at the beginning and inf at the end as sentinels. This ensures:

  • The first element is never replaced (it's already the smallest possible)
  • The last element is never replaced (it's already the largest possible)
  • We can focus on making the array strictly increasing up to the last real element

3. Dynamic Programming Setup:

f = [inf] * n
f[0] = 0

Initialize f[i] to represent the minimum operations needed to make arr[0...i] strictly increasing while keeping arr[i] unchanged. Set f[0] = 0 since the sentinel -inf requires no operations.

4. Main DP Transition: For each position i from 1 to n-1:

if arr[i - 1] < arr[i]:
    f[i] = f[i - 1]

First, check if we can keep both arr[i-1] and arr[i] without any replacements between them. If they already form an increasing pair, we can transfer the state directly.

j = bisect_left(arr2, arr[i])
for k in range(1, min(i - 1, j) + 1):
    if arr[i - k - 1] < arr2[j - k]:
        f[i] = min(f[i], f[i - k - 1] + k)

Then, consider replacing k consecutive elements before position i:

  • Use binary search to find index j - the first position in arr2 where the value is >= arr[i]
  • Try replacing k elements (from 1 to min(i-1, j)) before position i
  • For each k, we need k strictly increasing values from arr2 that are all less than arr[i]
  • These values would be arr2[j-k], arr2[j-k+1], ..., arr2[j-1] (the largest k values less than arr[i])
  • Check if arr[i-k-1] < arr2[j-k] to ensure the sequence remains strictly increasing
  • If valid, update f[i] with the minimum of current value and f[i-k-1] + k

5. Return Result:

return -1 if f[n - 1] >= inf else f[n - 1]

If f[n-1] is still inf, it means there's no way to make the array strictly increasing. Otherwise, return the minimum number of operations.

The time complexity is O(n^2 + m log m) where n is the length of arr1 and m is the length of arr2. The space complexity is O(n + m).

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example: arr1 = [1, 5, 3, 6] and arr2 = [4, 3, 1]

Step 1: Preprocess arr2

  • Sort arr2: [1, 3, 4]
  • Remove duplicates: [1, 3, 4] (no duplicates in this case)

Step 2: Add sentinels

  • arr = [-inf, 1, 5, 3, 6, inf]
  • Length n = 6

Step 3: Initialize DP array

  • f = [0, inf, inf, inf, inf, inf]
  • f[0] = 0 (no operations needed for sentinel)

Step 4: Fill DP array

Position i = 1 (value = 1):

  • Check if arr[0] < arr[1]: -inf < 1
  • Set f[1] = f[0] = 0 (can keep arr[1] = 1)
  • Binary search: j = bisect_left([1,3,4], 1) = 0
  • No replacements possible (j = 0)
  • Result: f[1] = 0

Position i = 2 (value = 5):

  • Check if arr[1] < arr[2]: 1 < 5
  • Set f[2] = f[1] = 0 (can keep arr[2] = 5)
  • Binary search: j = bisect_left([1,3,4], 5) = 3
  • Try k = 1: Replace 1 element before position 2
    • Need arr[0] < arr2[2]: -inf < 4
    • Update: f[2] = min(0, f[0] + 1) = 0
  • Result: f[2] = 0

Position i = 3 (value = 3):

  • Check if arr[2] < arr[3]: 5 < 3 ✗ (cannot directly keep both)
  • Binary search: j = bisect_left([1,3,4], 3) = 1
  • Try k = 1: Replace 1 element before position 3 (replace position 2)
    • Need arr[1] < arr2[0]: 1 < 1 ✗ (not valid)
  • Result: f[3] = inf (cannot keep value 3 at position 3)

Position i = 4 (value = 6):

  • Check if arr[3] < arr[4]: 3 < 6
  • Set f[4] = f[3] = inf (but f[3] is inf)
  • Binary search: j = bisect_left([1,3,4], 6) = 3
  • Try k = 1: Replace 1 element before position 4 (replace position 3)
    • Need arr[2] < arr2[2]: 5 < 4 ✗ (not valid)
  • Try k = 2: Replace 2 elements before position 4 (replace positions 2 and 3)
    • Need arr[1] < arr2[1]: 1 < 3
    • Update: f[4] = min(inf, f[1] + 2) = 0 + 2 = 2
  • Try k = 3: Replace 3 elements before position 4 (replace positions 1, 2, and 3)
    • Need arr[0] < arr2[0]: -inf < 1
    • Update: f[4] = min(2, f[0] + 3) = min(2, 3) = 2
  • Result: f[4] = 2

Position i = 5 (value = inf):

  • Check if arr[4] < arr[5]: 6 < inf
  • Set f[5] = f[4] = 2
  • Result: f[5] = 2

Step 5: Return result

  • f[5] = 2, which is less than inf
  • Return 2

The minimum number of operations is 2. We need to replace positions 2 and 3 in the original array:

  • Replace arr1[1] = 5 with arr2[1] = 3
  • Replace arr1[2] = 3 with arr2[2] = 4
  • Final array: [1, 3, 4, 6] which is strictly increasing

Solution Implementation

1from typing import List
2from bisect import bisect_left
3
4class Solution:
5    def makeArrayIncreasing(self, arr1: List[int], arr2: List[int]) -> int:
6        # Sort arr2 and remove duplicates
7        arr2.sort()
8        unique_count = 0
9        for value in arr2:
10            # Keep only unique values from arr2
11            if unique_count == 0 or value != arr2[unique_count - 1]:
12                arr2[unique_count] = value
13                unique_count += 1
14        arr2 = arr2[:unique_count]
15      
16        # Add sentinels to arr1 to simplify boundary conditions
17        # -inf at start ensures we can always start, inf at end ensures we can always end
18        extended_arr = [float('-inf')] + arr1 + [float('inf')]
19        n = len(extended_arr)
20      
21        # dp[i] = minimum operations needed to make extended_arr[0..i] strictly increasing
22        dp = [float('inf')] * n
23        dp[0] = 0  # No operations needed for the first sentinel
24      
25        # Process each position in the extended array
26        for i in range(1, n):
27            # Option 1: Keep current element if it's already greater than previous
28            if extended_arr[i - 1] < extended_arr[i]:
29                dp[i] = dp[i - 1]
30          
31            # Option 2: Try replacing previous k elements with values from arr2
32            # Find the position in arr2 where we could insert extended_arr[i]
33            max_replacement_idx = bisect_left(arr2, extended_arr[i])
34          
35            # Try replacing k consecutive elements before position i
36            for k in range(1, min(i - 1, max_replacement_idx) + 1):
37                # Check if we can replace elements [i-k, i-1] with arr2[max_replacement_idx-k:max_replacement_idx]
38                # This works if extended_arr[i-k-1] < arr2[max_replacement_idx-k] (maintains strict increasing)
39                if extended_arr[i - k - 1] < arr2[max_replacement_idx - k]:
40                    dp[i] = min(dp[i], dp[i - k - 1] + k)
41      
42        # Return -1 if impossible, otherwise return the minimum operations
43        return -1 if dp[n - 1] >= float('inf') else dp[n - 1]
44
1class Solution {
2    public int makeArrayIncreasing(int[] arr1, int[] arr2) {
3        // Sort arr2 and remove duplicates
4        Arrays.sort(arr2);
5        int uniqueCount = 0;
6        for (int num : arr2) {
7            // Keep only unique elements in arr2
8            if (uniqueCount == 0 || num != arr2[uniqueCount - 1]) {
9                arr2[uniqueCount++] = num;
10            }
11        }
12      
13        // Define infinity value for comparison
14        final int INFINITY = 1 << 30;
15      
16        // Create padded array with sentinels at both ends
17        // arr[0] = -infinity (smaller than any element)
18        // arr[n+1] = infinity (larger than any element)
19        int[] paddedArray = new int[arr1.length + 2];
20        paddedArray[0] = -INFINITY;
21        paddedArray[paddedArray.length - 1] = INFINITY;
22        System.arraycopy(arr1, 0, paddedArray, 1, arr1.length);
23      
24        // dp[i] represents minimum operations needed to make array valid up to index i
25        int[] dp = new int[paddedArray.length];
26        Arrays.fill(dp, INFINITY);
27        dp[0] = 0; // Base case: no operations needed for the sentinel
28      
29        // Dynamic programming to find minimum operations
30        for (int i = 1; i < paddedArray.length; ++i) {
31            // Case 1: Keep current element if it's greater than previous
32            if (paddedArray[i - 1] < paddedArray[i]) {
33                dp[i] = dp[i - 1];
34            }
35          
36            // Case 2: Try replacing consecutive elements from arr2
37            // Find the position in arr2 where elements are >= paddedArray[i]
38            int replaceEndIndex = binarySearch(arr2, paddedArray[i], uniqueCount);
39          
40            // Try replacing k consecutive elements before index i
41            for (int k = 1; k <= Math.min(i - 1, replaceEndIndex); ++k) {
42                // Check if we can replace k elements ending at position i-1
43                // with k largest elements from arr2 that are smaller than paddedArray[i]
44                if (paddedArray[i - k - 1] < arr2[replaceEndIndex - k]) {
45                    dp[i] = Math.min(dp[i], dp[i - k - 1] + k);
46                }
47            }
48        }
49      
50        // Return result: -1 if impossible, otherwise the minimum operations
51        return dp[paddedArray.length - 1] >= INFINITY ? -1 : dp[paddedArray.length - 1];
52    }
53  
54    /**
55     * Binary search to find the leftmost position where nums[pos] >= target
56     * @param nums sorted array to search in
57     * @param target value to search for
58     * @param length effective length of the array
59     * @return index of first element >= target, or length if all elements < target
60     */
61    private int binarySearch(int[] nums, int target, int length) {
62        int left = 0;
63        int right = length;
64      
65        while (left < right) {
66            int mid = (left + right) >> 1;
67            if (nums[mid] >= target) {
68                right = mid;
69            } else {
70                left = mid + 1;
71            }
72        }
73      
74        return left;
75    }
76}
77
1class Solution {
2public:
3    int makeArrayIncreasing(vector<int>& arr1, vector<int>& arr2) {
4        // Sort arr2 and remove duplicates to prepare for binary search
5        sort(arr2.begin(), arr2.end());
6        arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end());
7      
8        // Define infinity value for boundary conditions
9        const int INF = 1 << 30;
10      
11        // Add sentinel values at the beginning and end of arr1
12        // This helps handle boundary cases in the DP logic
13        arr1.insert(arr1.begin(), -INF);
14        arr1.push_back(INF);
15      
16        int n = arr1.size();
17      
18        // dp[i] = minimum operations needed to make arr1[0...i] strictly increasing
19        vector<int> dp(n, INF);
20        dp[0] = 0;  // Base case: no operations needed for the sentinel value
21      
22        // Process each position in arr1
23        for (int i = 1; i < n; ++i) {
24            // Case 1: Keep arr1[i] unchanged if it's already greater than arr1[i-1]
25            if (arr1[i - 1] < arr1[i]) {
26                dp[i] = dp[i - 1];
27            }
28          
29            // Case 2: Try replacing some elements before position i with elements from arr2
30            // Find the largest element in arr2 that is less than arr1[i]
31            int maxReplacementIdx = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin();
32          
33            // Try replacing k consecutive elements ending at position i-1
34            for (int k = 1; k <= min(i - 1, maxReplacementIdx); ++k) {
35                // Check if we can replace arr1[i-k] to arr1[i-1] with 
36                // arr2[maxReplacementIdx-k] to arr2[maxReplacementIdx-1]
37                // This works if arr1[i-k-1] < arr2[maxReplacementIdx-k] (maintains strict increasing)
38                if (arr1[i - k - 1] < arr2[maxReplacementIdx - k]) {
39                    dp[i] = min(dp[i], dp[i - k - 1] + k);
40                }
41            }
42        }
43      
44        // Return the result for the last position (excluding sentinel)
45        // If dp[n-1] is still INF, it means no valid solution exists
46        return dp[n - 1] >= INF ? -1 : dp[n - 1];
47    }
48};
49
1/**
2 * Makes arr1 strictly increasing by replacing elements with elements from arr2
3 * Returns the minimum number of operations needed, or -1 if impossible
4 */
5function makeArrayIncreasing(arr1: number[], arr2: number[]): number {
6    // Sort arr2 in ascending order
7    arr2.sort((a, b) => a - b);
8  
9    // Remove duplicates from arr2 to optimize search
10    let uniqueCount = 0;
11    for (const value of arr2) {
12        if (uniqueCount === 0 || value !== arr2[uniqueCount - 1]) {
13            arr2[uniqueCount++] = value;
14        }
15    }
16    arr2 = arr2.slice(0, uniqueCount);
17  
18    // Define infinity constant for boundary values
19    const INFINITY = 1 << 30;
20  
21    // Add boundary values to arr1 to simplify edge case handling
22    arr1 = [-INFINITY, ...arr1, INFINITY];
23    const arrayLength = arr1.length;
24  
25    // dp[i] represents minimum operations to make arr1[0...i] strictly increasing
26    const dp: number[] = new Array(arrayLength).fill(INFINITY);
27    dp[0] = 0;
28  
29    /**
30     * Binary search to find the first index where arr[index] >= target
31     */
32    const binarySearch = (arr: number[], target: number): number => {
33        let left = 0;
34        let right = arr.length;
35      
36        while (left < right) {
37            const mid = (left + right) >> 1;
38            if (arr[mid] >= target) {
39                right = mid;
40            } else {
41                left = mid + 1;
42            }
43        }
44        return left;
45    };
46  
47    // Process each position in arr1
48    for (let i = 1; i < arrayLength; ++i) {
49        // Case 1: Keep current element if it maintains strictly increasing property
50        if (arr1[i - 1] < arr1[i]) {
51            dp[i] = dp[i - 1];
52        }
53      
54        // Case 2: Try replacing previous k elements with elements from arr2
55        const availableReplacements = binarySearch(arr2, arr1[i]);
56      
57        // Try replacing k consecutive elements before position i
58        for (let k = 1; k <= Math.min(i - 1, availableReplacements); ++k) {
59            // Check if we can maintain strictly increasing after replacement
60            if (arr1[i - k - 1] < arr2[availableReplacements - k]) {
61                dp[i] = Math.min(dp[i], dp[i - k - 1] + k);
62            }
63        }
64    }
65  
66    // Return result: -1 if impossible, otherwise the minimum operations
67    return dp[arrayLength - 1] >= INFINITY ? -1 : dp[arrayLength - 1];
68}
69

Time and Space Complexity

Time Complexity: O(n × (log m + min(m, n)))

The algorithm consists of several key operations:

  • Sorting arr2 takes O(m log m) where m is the length of arr2
  • Removing duplicates from arr2 takes O(m) time
  • The main dynamic programming loop runs for n iterations (length of augmented arr)
  • For each position i:
    • Checking if arr[i-1] < arr[i] takes O(1)
    • Binary search using bisect_left takes O(log m)
    • The inner loop runs for min(i-1, j) iterations, which is bounded by min(m, n)
    • Each iteration of the inner loop performs constant time operations

Therefore, the dominant operation is the nested loop structure with binary search, giving us O(n × (log m + min(m, n))) time complexity.

Space Complexity: O(n)

The space usage includes:

  • The modified arr2 after removing duplicates uses O(m) space, but this modifies the input in-place
  • The augmented array arr uses O(n) space
  • The DP array f uses O(n) space
  • Other variables use O(1) space

Since we're measuring additional space beyond the input, and n is the length of arr1, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Duplicate Values in arr2

Pitfall: Using arr2 directly without removing duplicates can lead to inefficient binary search and incorrect replacement logic. When duplicates exist, the algorithm might consider the same value multiple times for replacement, which doesn't provide any benefit but complicates the logic.

Example:

arr1 = [1, 5, 3]
arr2 = [2, 2, 2, 4, 4]  # Contains duplicates

Solution: Always sort and deduplicate arr2 first:

arr2.sort()
unique_count = 0
for value in arr2:
    if unique_count == 0 or value != arr2[unique_count - 1]:
        arr2[unique_count] = value
        unique_count += 1
arr2 = arr2[:unique_count]

2. Incorrect Boundary Checking When Replacing Multiple Elements

Pitfall: When trying to replace k consecutive elements, failing to properly check if there are enough elements available in both arrays can cause index out of bounds errors or incorrect results.

Wrong approach:

for k in range(1, i):  # Could exceed available elements in arr2
    if extended_arr[i - k - 1] < arr2[max_replacement_idx - k]:
        # This might access arr2[-1] when max_replacement_idx < k

Solution: Use min(i - 1, max_replacement_idx) to ensure we don't exceed bounds:

for k in range(1, min(i - 1, max_replacement_idx) + 1):
    if extended_arr[i - k - 1] < arr2[max_replacement_idx - k]:
        dp[i] = min(dp[i], dp[i - k - 1] + k)

3. Forgetting to Check if Keeping Current Element is Valid

Pitfall: Always trying to replace elements without first checking if the current configuration already works.

Example:

# Wrong: Only considering replacements
for i in range(1, n):
    # Only replacement logic, missing the "keep current" option
    max_replacement_idx = bisect_left(arr2, extended_arr[i])
    for k in range(1, min(i - 1, max_replacement_idx) + 1):
        ...

Solution: Always check if keeping the current element maintains strict increasing order:

if extended_arr[i - 1] < extended_arr[i]:
    dp[i] = dp[i - 1]  # Can keep current element without replacement

4. Using Regular Infinity Instead of Float Infinity

Pitfall: Using a large integer like 10**9 as infinity can cause issues if the actual minimum operations exceed this value or when comparing with legitimate values.

Wrong:

dp = [10**9] * n  # Might not be large enough
extended_arr = [-10**9] + arr1 + [10**9]  # Might conflict with actual values

Solution: Use float('inf') and float('-inf'):

dp = [float('inf')] * n
extended_arr = [float('-inf')] + arr1 + [float('inf')]

5. Misunderstanding the Replacement Strategy

Pitfall: Thinking that we need to find the optimal replacement for each position independently, rather than considering consecutive replacements as a group.

Key Insight: When replacing k consecutive elements before position i, we should use the largest k values from arr2 that are less than extended_arr[i]. These are arr2[max_replacement_idx-k:max_replacement_idx], which ensures:

  • They form a strictly increasing sequence
  • They're all less than extended_arr[i]
  • They're the largest possible values satisfying these conditions (minimizing conflict with earlier elements)

Solution: The code correctly handles this by:

  1. Finding max_replacement_idx using binary search
  2. Taking k elements ending at max_replacement_idx-1
  3. Checking if the smallest of these (arr2[max_replacement_idx-k]) is greater than extended_arr[i-k-1]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More