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
fromarr1
(where0 <= i < arr1.length
) - Choose any index
j
fromarr2
(where0 <= j < arr2.length
) - Replace
arr1[i]
witharr2[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]
andarr2 = [1, 3, 2, 4]
, you could replacearr1[2]
(which is 3) witharr2[3]
(which is 4) to get[1, 5, 4, 6, 7]
. But this still isn't strictly increasing because5 > 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.
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:
- Find
k
values fromarr2
that form a strictly increasing sequence - These values must be less than
arr1[i]
- 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 inarr2
where the value is>= arr[i]
- Try replacing
k
elements (from 1 tomin(i-1, j)
) before positioni
- For each
k
, we needk
strictly increasing values fromarr2
that are all less thanarr[i]
- These values would be
arr2[j-k], arr2[j-k+1], ..., arr2[j-1]
(the largestk
values less thanarr[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 andf[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 EvaluatorExample 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
- Need
- 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)
- Need
- 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)
- Need
- 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
- Need
- 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
- Need
- 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
witharr2[1] = 3
- Replace
arr1[2] = 3
witharr2[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
takesO(m log m)
wherem
is the length ofarr2
- Removing duplicates from
arr2
takesO(m)
time - The main dynamic programming loop runs for
n
iterations (length of augmentedarr
) - For each position
i
:- Checking if
arr[i-1] < arr[i]
takesO(1)
- Binary search using
bisect_left
takesO(log m)
- The inner loop runs for
min(i-1, j)
iterations, which is bounded bymin(m, n)
- Each iteration of the inner loop performs constant time operations
- Checking if
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 usesO(m)
space, but this modifies the input in-place - The augmented array
arr
usesO(n)
space - The DP array
f
usesO(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:
- Finding
max_replacement_idx
using binary search - Taking
k
elements ending atmax_replacement_idx-1
- Checking if the smallest of these (
arr2[max_replacement_idx-k]
) is greater thanextended_arr[i-k-1]
In a binary min heap, the minimum element can be found in:
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!