Minimum Operations to Make a Subsequence
HardPrompt
You are given an array target that consists of distinct integers and another integer array arr that can have duplicates.
In one operation, you can insert any integer at any position in arr. For example, if arr = [1,4,1,2], you can add 3 in the middle and make it [1,4,3,1,2]. Note that you can insert the integer at the very beginning or end of the array.
Return the minimum number of operations needed to make target a subsequence of arr.
A subsequence of an array is a new array generated from the original array by deleting some elements (possibly none) without changing the remaining elements' relative order. For example, [2,7,4] is a subsequence of [4,2,3,7,2,1,4] (the underlined elements), while [2,4,2] is not.
Example 1:
arrExample 2:
arr
Constraints:
1 <= target.length, arr.length <= 1051 <= target[i], arr[i] <= 109targetcontains no duplicates.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach correctly identifies that the problem can be transformed from finding a Longest Common Subsequence (LCS) to finding a Longest Increasing Subsequence (LIS). By mapping the values in the target array to their indices, we can create a new sequence from arr consisting only of these indices. An increasing subsequence in this new sequence corresponds to a valid common subsequence in the original arrays. This approach then uses a standard, but less efficient, dynamic programming algorithm with O(N^2) time complexity to find the length of the LIS.
Algorithm
- The core idea is that minimizing insertions is equivalent to maximizing the number of elements from
targetthat are already present inarrin the correct relative order. This is the definition of the Longest Common Subsequence (LCS). - However, a standard O(m*n) LCS algorithm is too slow. We can optimize by using the fact that
targetcontains distinct elements. - This allows us to reframe the problem as finding the Longest Increasing Subsequence (LIS).
- Map
targetvalues to indices: Create aHashMapto store each number intargetand its corresponding index. This lets us quickly check if a number fromarris intargetand what its relative order should be. - Create an index sequence: Iterate through
arr. For each number that also exists intarget, find its index from the map and add it to a new list, let's call itindices. - Find LIS of
indices: The problem is now reduced to finding the length of the LIS of thisindiceslist. An increasing subsequence of these indices corresponds to elements fromtargetappearing in the correct relative order withinarr. - Calculate LIS with O(N^2) DP:
- Create a
dparray of the same size asindices, wheredp[i]stores the length of the LIS ending atindices[i]. - Initialize all
dpvalues to 1. - Iterate through the
indiceslist from the second element (i=1). For each element, iterate through all preceding elements (j < i). - If
indices[i] > indices[j], it means we can extend the subsequence ending atj. Updatedp[i]withmax(dp[i], 1 + dp[j]). - The maximum value in the
dparray is the length of the LIS.
- Create a
- Calculate result: The number of required insertions is
target.length - length(LIS).
Walkthrough
The minimum number of operations is target.length - length(LCS(target, arr)). Since target has distinct elements, we can simplify the LCS problem. We first create a map from each element in target to its index. Then, we iterate through arr and form a new list, indices, containing the indices of elements that are present in both arr and target. The length of the LCS is equal to the length of the Longest Increasing Subsequence (LIS) of this indices list. This approach calculates the LIS length using a straightforward but slow O(N^2) dynamic programming solution.
import java.util.HashMap;import java.util.ArrayList;import java.util.List;import java.util.Arrays; class Solution { public int minOperations(int[] target, int[] arr) { HashMap<Integer, Integer> targetMap = new HashMap<>(); for (int i = 0; i < target.length; i++) { targetMap.put(target[i], i); } List<Integer> indices = new ArrayList<>(); for (int num : arr) { if (targetMap.containsKey(num)) { indices.add(targetMap.get(num)); } } if (indices.isEmpty()) { return target.length; } int[] dp = new int[indices.size()]; Arrays.fill(dp, 1); int maxLis = 1; for (int i = 1; i < indices.size(); i++) { for (int j = 0; j < i; j++) { if (indices.get(i) > indices.get(j)) { dp[i] = Math.max(dp[i], 1 + dp[j]); } } maxLis = Math.max(maxLis, dp[i]); } return target.length - maxLis; }}Complexity
Time
O(m + n + k^2), where `m` is `target.length`, `n` is `arr.length`, and `k` is the size of the generated `indices` list (`k <= n`). This simplifies to O(m + n^2) in the worst case.
Space
O(m + k), where `m` is the length of `target` and `k` is the number of elements in `arr` also present in `target`. In the worst case, this is O(m + n).
Trade-offs
Pros
Correctly reduces the problem from LCS to LIS, which is a key insight.
The DP logic for LIS is relatively simple to understand and implement.
Cons
The O(k^2) complexity for the LIS calculation is too slow for the given constraints (k can be up to 10^5), leading to a 'Time Limit Exceeded' error on large test cases.
This optimal approach uses the same problem reduction as the previous one, transforming the LCS problem into an LIS problem. However, it employs a highly efficient O(N log N) algorithm to find the length of the Longest Increasing Subsequence. This algorithm, sometimes known as Patience Sorting, uses a helper list and binary search. For each element of the generated index sequence, it either extends the LIS or updates the list to allow for a future LIS with a smaller tail element. This avoids the O(N^2) complexity and is fast enough to pass all test cases within the given constraints.
Algorithm
- Map
targetvalues to indices: Just like the previous approach, create aHashMapto store each number intargetand its corresponding index. This takes O(m) time. - Process
arrto find LIS directly: Instead of creating an intermediateindiceslist, we can compute the LIS on the fly. We use an efficient O(N log N) algorithm. - Find LIS with Binary Search (Patience Sorting):
- Initialize an empty list,
sub, which will store the smallest tail of all increasing subsequences of a certain length. The length ofsubwill be the length of the LIS. - Iterate through each number
numinarr. - If
numis not in thetargetMap, ignore it. - Get the index
idx = targetMap.get(num). - Perform a binary search on the
sublist to find the first element that is greater than or equal toidx. This is the position whereidxcan replace an existing element to form an increasing subsequence of the same length but with a smaller tail, or where it can be inserted to extend the LIS. - If the binary search finds that
idxis larger than all elements insub, it means we can extend the longest subsequence found so far. Appendidxtosub. - Otherwise, replace the element at the found position with
idx.
- Initialize an empty list,
- Calculate result: After iterating through
arr, the size of thesublist is the length of the LCS. The final answer istarget.length - sub.size().
Walkthrough
This solution refines the previous approach by using a much faster algorithm for finding the LIS length. The initial steps of mapping target elements to indices are the same. The core improvement is in the LIS calculation. We maintain a list, sub, that represents the tails of potential increasing subsequences. For each index from arr, we use binary search to find its place in sub. If the index is larger than any tail in sub, it extends the LIS. Otherwise, it replaces the smallest tail that is greater than or equal to it, creating a new potential LIS of the same length with a more favorable (smaller) tail.
import java.util.HashMap;import java.util.ArrayList;import java.util.List; class Solution { public int minOperations(int[] target, int[] arr) { HashMap<Integer, Integer> targetMap = new HashMap<>(); for (int i = 0; i < target.length; i++) { targetMap.put(target[i], i); } List<Integer> sub = new ArrayList<>(); for (int num : arr) { if (!targetMap.containsKey(num)) { continue; } int index = targetMap.get(num); // Binary search to find the insertion point for 'index' in 'sub' int left = 0, right = sub.size(); while (left < right) { int mid = left + (right - left) / 2; if (sub.get(mid) < index) { left = mid + 1; } else { right = mid; } } if (left == sub.size()) { // If index is greater than all elements in sub, extend the subsequence sub.add(index); } else { // Replace the element at 'left' to potentially form a better subsequence sub.set(left, index); } } return target.length - sub.size(); }}Complexity
Time
O(m + n log k), where `m` is `target.length`, `n` is `arr.length`, and `k` is the length of the LIS (`k <= m`). Since `k <= n`, the complexity is bounded by O(m + n log n).
Space
O(m + k), where `m` is `target.length` and `k` is the length of the LIS. In the worst case, this is O(m + min(m, n)).
Trade-offs
Pros
Highly efficient with O(m + n log n) time complexity, which is optimal for this problem.
Effectively handles the large constraints of the problem.
Cons
The logic for the O(N log N) LIS algorithm is more complex and less intuitive than the O(N^2) DP approach.
Solutions
Solution
class BinaryIndexedTree { private int n ; private int [] c ; public BinaryIndexedTree ( int n ) { this . n = n ; this . c = new int [ n + 1 ]; } public static int lowbit ( int x ) { return x & - x ; } public void update ( int x , int val ) { while ( x <= n ) { c [ x ] = Math . max ( c [ x ], val ); x += lowbit ( x ); } } public int query ( int x ) { int s = 0 ; while ( x > 0 ) { s = Math . max ( s , c [ x ]); x -= lowbit ( x ); } return s ; } } class Solution { public int minOperations ( int [] target , int [] arr ) { Map < Integer , Integer > d = new HashMap <>(); for ( int i = 0 ; i < target . length ; ++ i ) { d . put ( target [ i ], i ); } List < Integer > nums = new ArrayList <>(); for ( int i = 0 ; i < arr . length ; ++ i ) { if ( d . containsKey ( arr [ i ])) { nums . add ( d . get ( arr [ i ])); } } return target . length - lengthOfLIS ( nums ); } private int lengthOfLIS ( List < Integer > nums ) { TreeSet < Integer > ts = new TreeSet (); for ( int v : nums ) { ts . add ( v ); } int idx = 1 ; Map < Integer , Integer > d = new HashMap <>(); for ( int v : ts ) { d . put ( v , idx ++); } int ans = 0 ; BinaryIndexedTree tree = new BinaryIndexedTree ( nums . size ()); for ( int v : nums ) { int x = d . get ( v ); int t = tree . query ( x - 1 ) + 1 ; ans = Math . max ( ans , t ); tree . update ( x , t ); } return ans ; } }class BinaryIndexedTree { public: int n ; vector < int > c ; BinaryIndexedTree ( int _n ) : n ( _n ) , c ( _n + 1 ) {} void update ( int x , int val ) { while ( x <= n ) { c [ x ] = max ( c [ x ], val ); x += lowbit ( x ); } } int query ( int x ) { int s = 0 ; while ( x > 0 ) { s = max ( s , c [ x ]); x -= lowbit ( x ); } return s ; } int lowbit ( int x ) { return x & - x ; } }; class Solution { public: int minOperations ( vector < int >& target , vector < int >& arr ) { unordered_map < int , int > d ; for ( int i = 0 ; i < target . size (); ++ i ) d [ target [ i ]] = i ; vector < int > nums ; for ( int i = 0 ; i < arr . size (); ++ i ) { if ( d . count ( arr [ i ])) { nums . push_back ( d [ arr [ i ]]); } } return target . size () - lengthOfLIS ( nums ); } int lengthOfLIS ( vector < int >& nums ) { set < int > s ( nums . begin (), nums . end ()); int idx = 1 ; unordered_map < int , int > d ; for ( int v : s ) d [ v ] = idx ++ ; BinaryIndexedTree * tree = new BinaryIndexedTree ( d . size ()); int ans = 0 ; for ( int v : nums ) { int x = d [ v ]; int t = tree -> query ( x - 1 ) + 1 ; ans = max ( ans , t ); tree -> update ( x , t ); } return ans ; } };class BinaryIndexedTree : def __init__ ( self , n ): self . n = n self . c = [ 0 ] * ( n + 1 ) @ staticmethod def lowbit ( x ): return x & - x def update ( self , x , val ): while x <= self . n : self . c [ x ] = max ( self . c [ x ], val ) x += BinaryIndexedTree . lowbit ( x ) def query ( self , x ): s = 0 while x : s = max ( s , self . c [ x ]) x -= BinaryIndexedTree . lowbit ( x ) return s class Solution : def minOperations ( self , target : List [ int ], arr : List [ int ]) -> int : d = { v : i for i , v in enumerate ( target )} nums = [ d [ v ] for v in arr if v in d ] return len ( target ) - self . lengthOfLIS ( nums ) def lengthOfLIS ( self , nums ): s = sorted ( set ( nums )) m = { v : i for i , v in enumerate ( s , 1 )} tree = BinaryIndexedTree ( len ( m )) ans = 0 for v in nums : x = m [ v ] t = tree . query ( x - 1 ) + 1 ans = max ( ans , t ) tree . update ( x , t ) return ansVideo 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.