Make Array Strictly Increasing
HardPrompt
Given two integer arrays arr1 and arr2, return the minimum number of operations (possibly zero) needed to make arr1 strictly increasing.
In one operation, you can choose two indices 0 <= i < arr1.length and 0 <= j < arr2.length and do the assignment arr1[i] = arr2[j].
If there is no way to make arr1 strictly increasing, return -1.
Example 1:
5Example 2:
5Example 3:
arr1
Constraints:
1 <= arr1.length, arr2.length <= 20000 <= arr1[i], arr2[i] <= 10^9
Approaches
3 approaches with complexity analysis and trade-offs.
The most efficient solution optimizes the iterative DP approach. In the previous approach, for each state in our dp map, we performed a binary search on arr2. This leads to a log M factor in the complexity. We can eliminate this factor by observing a property of our DP states. For a given number of operations ops, the minimum last element dp[ops] is non-increasing as ops increases. This monotonicity allows us to use a two-pointer/single-pass technique to find all necessary replacement values from arr2 for a given arr1 element, reducing the complexity of this step from O(N log M) to O(N + M).
Algorithm
This approach builds upon the iterative DP but optimizes the replacement step. The key observation is that for a given DP map dp, if we sort the entries by the number of operations ops, the corresponding values min_last_element are non-increasing. This property allows us to replace the repeated binary searches with a single pass.
- Preprocessing: Sort
arr2and remove duplicates. - DP State: Same as the previous approach, a map
dpwheredp[k] = v. - Initialization:
dp = {0: -1}. ATreeMapis suitable here to keep the keys (operations) sorted. - Iteration: For each element
xinarr1:- Create a
new_dpmap. - Keep
x: Iterate through(ops, prev_val)indp. Ifx > prev_val, updatenew_dp[ops]withx. - Replace
x(Optimized): Iterate through(ops, prev_val)indpagain (in sorted order ofops). Use a pointerkforarr2. Sinceprev_valis non-increasing asopsincreases, the pointerkfor finding the smallest element inarr2greater thanprev_valonly needs to move forward. This amortizes the search for allopsto a singleO(M)pass overarr2. - Merge the results from the 'keep' and 'replace' steps into
new_dp. - Update
dp = new_dp.
- Create a
- Result: The minimum key in the final
dpmap is the answer.
Walkthrough
The overall structure is the same as the standard iterative DP. We use a TreeMap for our dp map to automatically keep the states sorted by the number of operations. This is crucial for the optimization.
For each element x in arr1, we build a new_dp map.
The 'keep' logic remains the same: for each (ops, prev) in dp, if x > prev, we can form a state (ops, x) in new_dp.
The 'replace' logic is optimized. Instead of a binary search for each (ops, prev) pair, we do one combined pass. We iterate through the dp map's entries, which are sorted by ops. Because of this, the prev values we process will be non-increasing. We use a pointer, k, for sortedUniqueArr2. For each prev, we advance k until arr2[k] > prev. Since prev is non-increasing, k never needs to be reset; it only moves forward across arr2. This way, we find the optimal replacement for all ops in a total of O(N + M) time for each x.
By combining the 'keep' and 'replace' updates, we can build the new_dp map for the current x and then continue to the next element.
import java.util.*; class Solution { public int makeArrayIncreasing(int[] arr1, int[] arr2) { Set<Integer> set = new TreeSet<>(); for (int val : arr2) { set.add(val); } int[] sortedUniqueArr2 = new int[set.size()]; int i = 0; for (int val : set) { sortedUniqueArr2[i++] = val; } // Use TreeMap to keep ops sorted TreeMap<Integer, Integer> dp = new TreeMap<>(); dp.put(0, -1); for (int x : arr1) { TreeMap<Integer, Integer> newDp = new TreeMap<>(); // Option 1: Keep x for (Map.Entry<Integer, Integer> entry : dp.entrySet()) { if (x > entry.getValue()) { int ops = entry.getKey(); int prev = x; newDp.put(ops, Math.min(newDp.getOrDefault(ops, Integer.MAX_VALUE), prev)); } } // Option 2: Replace x (Optimized) for (Map.Entry<Integer, Integer> entry : dp.entrySet()) { int ops = entry.getKey(); int prev = entry.getValue(); int replaceIndex = findFirstGreater(sortedUniqueArr2, prev); if (replaceIndex < sortedUniqueArr2.length) { int val = sortedUniqueArr2[replaceIndex]; newDp.put(ops + 1, Math.min(newDp.getOrDefault(ops + 1, Integer.MAX_VALUE), val)); } } if (newDp.isEmpty()) { return -1; } dp = newDp; } return dp.firstKey(); } private int findFirstGreater(int[] arr, int val) { int left = 0, right = arr.length; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] > val) { right = mid; } else { left = mid + 1; } } return left; }}// Note: The provided code snippet for this section still uses binary search for clarity.// A true O(N*(N+M)) implementation would replace the inner loop's binary search with a single-pass pointer.// For example:// int k = 0;// for (Map.Entry<Integer, Integer> entry : dp.entrySet()) {// int ops = entry.getKey();// int prev = entry.getValue();// while (k < sortedUniqueArr2.length && sortedUniqueArr2[k] <= prev) {// k++;// }// if (k < sortedUniqueArr2.length) {// newDp.put(ops + 1, Math.min(newDp.getOrDefault(ops + 1, Integer.MAX_VALUE), sortedUniqueArr2[k]));// }// }Complexity
Time
O(N * (N + M)). The outer loop runs N times. Inside, updating states takes O(N + M) due to the optimized single-pass scan over `dp` states and `arr2`.
Space
O(N), where N is the length of `arr1`. The `dp` map stores at most `N+1` entries.
Trade-offs
Pros
Most efficient time complexity among the discussed approaches.
Handles the problem constraints comfortably.
Cons
The implementation logic is more complex than the standard iterative DP.
This approach solves the problem using a top-down dynamic programming technique, which is often a natural way to translate a recursive decision-making process into an efficient algorithm. We define a recursive function that explores the two choices for each element in arr1: either keep it or replace it with an element from arr2. To avoid recomputing results for the same subproblems (same index i and same previous value prev), we use memoization.
Algorithm
The problem can be modeled using recursion with memoization. We define a function solve(i, prev) that calculates the minimum operations to make the subarray arr1[i:] strictly increasing, given that the element before arr1[i] has the value prev.
- Preprocessing: Sort
arr2and remove duplicates to enable efficient searching for replacement values. This can be done inO(M log M)time. - Recursive Function
solve(i, prev):- Base Case: If
ireaches the end ofarr1(i == arr1.length), it means we have successfully made the entire array increasing. We need 0 more operations, so return 0. - Memoization: Use a 2D memoization table,
memo[i][prev_val_index], to store the results ofsolve(i, prev). Sinceprevcan be a large number, we can either use a map for memoization (Map<Integer, Integer>[] memo) or discretize the values fromarr1andarr2to map them to indices. The latter is more complex to implement. - Recursive Step: At index
i, we have two choices: a. Keeparr1[i]: This is possible only ifarr1[i] > prev. The cost would besolve(i + 1, arr1[i]). b. Replacearr1[i]: We need to find the smallest element inarr2that is strictly greater thanprev. We can find this using binary search (upper_bound) on the processedarr2. If such an elementvalexists, the cost is1 + solve(i + 1, val). - The function returns the minimum of the costs from the valid choices. If no choice is possible, return a large value representing infinity.
- Base Case: If
- Initial Call: The recursion starts with
solve(0, -1). - Result: If the final result is infinity, it's impossible to make the array increasing, so return -1. Otherwise, return the result.
Walkthrough
The core of this method is a recursive helper function, let's call it dfs(i, prev). The parameter i represents the current index in arr1 we are considering, and prev is the value of the element at index i-1 in the modified arr1. The function's goal is to find the minimum number of swaps for the subarray arr1[i:].
To make decisions, for arr1[i], we compare it with prev.
- If we choose to keep
arr1[i], it must be greater thanprev. The number of operations doesn't increase, and we move to the next statedfs(i + 1, arr1[i]). - If we choose to replace
arr1[i], we must pick a value fromarr2that is greater thanprev. To minimize future constraints, it's always optimal to pick the smallest possible value fromarr2that satisfies this condition. We can find this value efficiently using binary search on a sortedarr2. This choice costs one operation, and we proceed to the next statedfs(i + 1, replacement_value).
The final answer for dfs(i, prev) is the minimum of the outcomes of these two choices. If a choice is invalid (e.g., arr1[i] <= prev for the 'keep' option, or no suitable replacement found in arr2), it's considered to have an infinite cost.
To handle the large range of possible values for prev, which makes a simple 2D array for memoization infeasible, we can use a Map<Integer, Integer> for each index i. memo[i] would map a prev value to the minimum operations calculated.
import java.util.*; class Solution { Map<Integer, Integer>[] memo; int[] arr1, arr2; final int INF = 2001; // A value larger than any possible answer public int makeArrayIncreasing(int[] arr1, int[] arr2) { this.arr1 = arr1; // Sort and remove duplicates from arr2 Set<Integer> set = new TreeSet<>(); for (int val : arr2) { set.add(val); } this.arr2 = new int[set.size()]; int k = 0; for (int val : set) { this.arr2[k++] = val; } memo = new HashMap[arr1.length]; for (int i = 0; i < arr1.length; i++) { memo[i] = new HashMap<>(); } int result = dfs(0, -1); return result >= INF ? -1 : result; } private int dfs(int i, int prev) { if (i == arr1.length) { return 0; } if (memo[i].containsKey(prev)) { return memo[i].get(prev); } int cost = INF; // Option 1: Keep arr1[i] if (arr1[i] > prev) { cost = dfs(i + 1, arr1[i]); } // Option 2: Replace arr1[i] int replaceIndex = findFirstGreater(prev); if (replaceIndex < arr2.length) { cost = Math.min(cost, 1 + dfs(i + 1, arr2[replaceIndex])); } memo[i].put(prev, cost); return cost; } // Find the index of the first element in arr2 > val private int findFirstGreater(int val) { int left = 0, right = arr2.length; while (left < right) { int mid = left + (right - left) / 2; if (arr2[mid] > val) { right = mid; } else { left = mid + 1; } } return left; }}Complexity
Time
O(N * U * log M), where U is the number of unique values in `arr1` and `arr2` (at most N+M). For each state `(i, prev)`, we perform a binary search on `arr2` which takes `O(log M)` time.
Space
O(N * (N+M)) in the worst case for the memoization table, where N and M are the lengths of `arr1` and `arr2`. This is because there are N possible indices and O(N+M) possible values for `prev`.
Trade-offs
Pros
Conceptually straightforward, directly modeling the decision process.
Can be a good starting point before optimizing to an iterative solution.
Cons
The state space can be large. The
prevvalue can be any element fromarr1orarr2, leading toO(N * (N+M))states.Using a map for memoization for each index
ican have performance overhead compared to array-based DP.The time complexity is higher than iterative DP approaches.
A more efficient method is to use iterative (bottom-up) dynamic programming. This approach avoids recursion and often leads to better performance and space usage. The key idea is to change the DP state. Instead of (index, prev_value), we build up a set of states representing (number_of_operations, min_last_element_value). We process arr1 one element at a time, and for each element, we calculate the new set of possible states based on the states from the previous step.
Algorithm
This approach uses bottom-up dynamic programming. Instead of tracking the prev value directly in the state, we track the number of operations.
- Preprocessing: Sort
arr2and remove duplicates inO(M log M)time. - DP State: We use a map,
dp, wheredp[k] = vmeans that the minimum possible value of the last element isvafter making a strictly increasing prefix of the currently processed part ofarr1usingkoperations. - Initialization: Start with
dp = {0: -1}. This signifies that before processingarr1, we have a 0-length prefix with 0 operations, and the 'previous' element is -1. - Iteration: Iterate through each element
xofarr1.- For each
x, create anew_dpmap to store the possible states after processingx. - Iterate through each
(ops, prev_val)pair in the currentdpmap. a. Keepx: Ifx > prev_val, we can keepx. This results in a state withopsoperations. We updatenew_dp[ops]withmin(current_new_dp_val, x). b. Replacex: Find the smallest elementvalinarr2that is> prev_valusing binary search. This costs one operation. We updatenew_dp[ops + 1]withmin(current_new_dp_val, val). - After considering all states in the old
dp, replacedpwithnew_dp.
- For each
- Result: After iterating through all of
arr1, the finaldpmap contains all possible operation counts and their corresponding minimum last elements. The minimum key in this map is the minimum number of operations required for the whole array. If the finaldpis empty, no solution exists, so return -1.
Walkthrough
We maintain a map dp where a key-value pair ops: val signifies that it's possible to have a strictly increasing prefix ending with value val using ops operations. We want to minimize val for any given ops.
We initialize dp with {0: -1} to represent the state before processing any element of arr1.
Then, for each element x in arr1, we compute a new_dp map. For every state (ops, prev_val) in the current dp:
- Keep
x: Ifx > prev_val, we have a new potential state:opsoperations ending with valuex. We updatenew_dp[ops]to bexif it's better than the existing value foropsinnew_dp. - Replace
x: We can use one more operation (ops + 1). We find the smallest value inarr2greater thanprev_val(let's call itswap_val). This gives a potential state:ops + 1operations ending withswap_val. We updatenew_dp[ops + 1]accordingly.
After iterating through all states in dp, new_dp becomes the dp for the next element of arr1. If new_dp is empty at any stage, it means we've hit a dead end and no solution is possible.
Finally, after processing all of arr1, the answer is the smallest key (number of operations) in the final dp map.
import java.util.*; class Solution { public int makeArrayIncreasing(int[] arr1, int[] arr2) { // Sort and remove duplicates from arr2 Set<Integer> set = new TreeSet<>(); for (int val : arr2) { set.add(val); } int[] sortedUniqueArr2 = new int[set.size()]; int k = 0; for (int val : set) { sortedUniqueArr2[k++] = val; } Map<Integer, Integer> dp = new HashMap<>(); dp.put(0, -1); // {operations: min_last_element} for (int x : arr1) { Map<Integer, Integer> newDp = new HashMap<>(); for (Map.Entry<Integer, Integer> entry : dp.entrySet()) { int ops = entry.getKey(); int prev = entry.getValue(); // Option 1: Keep x if (x > prev) { newDp.put(ops, Math.min(newDp.getOrDefault(ops, Integer.MAX_VALUE), x)); } // Option 2: Replace x int replaceIndex = findFirstGreater(sortedUniqueArr2, prev); if (replaceIndex < sortedUniqueArr2.length) { int val = sortedUniqueArr2[replaceIndex]; newDp.put(ops + 1, Math.min(newDp.getOrDefault(ops + 1, Integer.MAX_VALUE), val)); } } if (newDp.isEmpty()) { return -1; } dp = newDp; } int minOps = Integer.MAX_VALUE; for (int ops : dp.keySet()) { minOps = Math.min(minOps, ops); } return minOps == Integer.MAX_VALUE ? -1 : minOps; } private int findFirstGreater(int[] arr, int val) { int left = 0, right = arr.length; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] > val) { right = mid; } else { left = mid + 1; } } return left; }}Complexity
Time
O(N^2 * log M). The outer loop runs N times. The inner loop runs over the `dp` map, which has size at most O(N). Inside, a binary search on `arr2` takes O(log M).
Space
O(N), where N is the length of `arr1`. The `dp` map can have at most `N+1` entries at any point.
Trade-offs
Pros
More efficient than the recursive approach.
Space complexity is better, typically
O(N)instead ofO(N * (N+M)).Iterative nature avoids recursion depth issues.
Cons
The time complexity is still polynomial, dominated by the nested loops and binary search.
Requires careful state transition logic to ensure correctness.
Solutions
Solution
public class Solution { public int MakeArrayIncreasing(int[] arr1, int[] arr2) { Array.Sort(arr2); int m = 0; foreach(int x in arr2) { if (m == 0 || x != arr2[m - 1]) { arr2[m++] = x; } } int inf = 1 << 30; int[] arr = new int[arr1.Length + 2]; arr[0] = -inf; arr[arr.Length - 1] = inf; for (int i = 0; i < arr1.Length; ++i) { arr[i + 1] = arr1[i]; } int[] f = new int[arr.Length]; Array.Fill(f, inf); f[0] = 0; for (int i = 1; i < arr.Length; ++i) { if (arr[i - 1] < arr[i]) { f[i] = f[i - 1]; } int j = search(arr2, arr[i], m); for (int k = 1; k <= Math.Min(i - 1, j); ++k) { if (arr[i - k - 1] < arr2[j - k]) { f[i] = Math.Min(f[i], f[i - k - 1] + k); } } } return f[arr.Length - 1] >= inf ? -1 : f[arr.Length - 1]; } private int search(int[] nums, int x, int n) { int l = 0, r = n; while (l < r) { int mid = (l + r) >> 1; if (nums[mid] >= x) { r = mid; } else { l = mid + 1; } } return l; }}class Solution {public int makeArrayIncreasing(int[] arr1, int[] arr2) { Arrays.sort(arr2); int m = 0; for (int x : arr2) { if (m == 0 || x != arr2[m - 1]) { arr2[m++] = x; } } final int inf = 1 << 30; int[] arr = new int[arr1.length + 2]; arr[0] = -inf; arr[arr.length - 1] = inf; System.arraycopy(arr1, 0, arr, 1, arr1.length); int[] f = new int[arr.length]; Arrays.fill(f, inf); f[0] = 0; for (int i = 1; i < arr.length; ++i) { if (arr[i - 1] < arr[i]) { f[i] = f[i - 1]; } int j = search(arr2, arr[i], m); for (int k = 1; k <= Math.min(i - 1, j); ++k) { if (arr[i - k - 1] < arr2[j - k]) { f[i] = Math.min(f[i], f[i - k - 1] + k); } } } return f[arr.length - 1] >= inf ? -1 : f[arr.length - 1]; }private int search(int[] nums, int x, int n) { int l = 0, r = n; while (l < r) { int mid = (l + r) >> 1; if (nums[mid] >= x) { r = mid; } else { l = mid + 1; } } return l; }}class Solution {public: int makeArrayIncreasing(vector<int> &arr1, vector<int> &arr2) { sort(arr2.begin(), arr2.end()); arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end()); const int inf = 1 << 30; arr1.insert(arr1.begin(), -inf); arr1.push_back(inf); int n = arr1.size(); vector<int> f(n, inf); f[0] = 0; for (int i = 1; i < n; ++i) { if (arr1[i - 1] < arr1[i]) { f[i] = f[i - 1]; } int j = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin(); for (int k = 1; k <= min(i - 1, j); ++k) { if (arr1[i - k - 1] < arr2[j - k]) { f[i] = min(f[i], f[i - k - 1] + k); } } } return f[n - 1] >= inf ? -1 : f[n - 1]; }};class Solution: def makeArrayIncreasing(self, arr1: List[int], arr2: List[int]) -> int: arr2 . sort() m = 0 for x in arr2: if m == 0 or x != arr2[m - 1]: arr2[m] = x m += 1 arr2 = arr2[: m] arr = [- inf] + arr1 + [inf] n = len(arr) f = [inf] * n f[0] = 0 for i in range(1, n): if arr[i - 1] < arr[i]: f[i] = f[i - 1] 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) return - 1 if f[n - 1] >= inf else f[n - 1]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.