1713. Minimum Operations to Make a Subsequence
Problem Description
You have two integer arrays: target
(containing distinct integers) and arr
(which may contain duplicates).
You can perform operations to insert any integer at any position in arr
. For instance, if arr = [1,4,1,2]
, you could insert 3
in the middle to get [1,4,3,1,2]
. Insertions can be made at the beginning, end, or anywhere in between.
Your goal is to find the minimum number of operations (insertions) needed to make target
a subsequence of arr
.
A subsequence is formed by deleting some (or no) elements from the original array without changing the order of the remaining elements. For example:
[2,7,4]
is a subsequence of[4,2,3,7,2,1,4]
(taking the 2nd, 4th, and 7th elements)[2,4,2]
is NOT a subsequence of the same array (the relative order isn't preserved)
The key insight is that you want to maximize the elements from target
that already appear in arr
in the correct relative order. The remaining elements from target
that aren't part of this longest common subsequence will need to be inserted.
For example, if target = [5,1,3]
and arr = [9,4,2,3,4]
, the element 3
from target already exists in arr
. So you only need to insert 5
and 1
(2 operations) to make target
a subsequence of the modified arr
.
Intuition
The key observation is that we want to minimize insertions, which means we should maximize the use of elements that already exist in arr
. If some elements from target
already appear in arr
in the correct relative order, we don't need to insert them - we only need to insert the missing ones.
This naturally leads us to think about the longest common subsequence (LCS) between target
and arr
. The elements in the LCS are already present in the right order, so we only need to insert the remaining len(target) - len(LCS)
elements.
However, finding LCS directly has time complexity O(m × n)
, which is too slow for large inputs. We need a clever transformation.
Since target
contains distinct elements, we can map each element to its position in target
. For example, if target = [5,1,3]
, we create a mapping: {5:0, 1:1, 3:2}
.
Now, when we scan through arr
, we can convert each element that exists in target
to its corresponding index. This gives us a new sequence nums
containing indices. For instance, if arr = [9,3,1,3]
and target = [5,1,3]
, we get nums = [2,1,2]
(ignoring 9 since it's not in target).
The crucial insight: Finding the LCS between target
and arr
is equivalent to finding the longest increasing subsequence (LIS) in nums
. Why? Because:
- If elements appear in the correct order in
arr
(matching their order intarget
), their corresponding indices innums
will be increasing - The longest such increasing sequence represents the maximum number of elements we can keep from
arr
This transformation reduces our problem from LCS O(m × n)
to LIS, which can be solved efficiently using Binary Indexed Tree or dynamic programming with binary search in O(n log n)
time.
Learn more about Greedy and Binary Search patterns.
Solution Approach
The solution implements the LIS (Longest Increasing Subsequence) approach using a Binary Indexed Tree (Fenwick Tree) for efficient range maximum queries and updates.
Step 1: Create Index Mapping
d = {x: i for i, x in enumerate(target, 1)}
We create a dictionary mapping each element in target
to its 1-based index position. Using 1-based indexing simplifies the Binary Indexed Tree implementation.
Step 2: Transform arr to Index Sequence
nums = [d[x] for x in arr if x in d]
We convert elements from arr
that exist in target
to their corresponding indices. Elements not in target
are ignored since they can't contribute to the common subsequence.
Step 3: Find LIS using Binary Indexed Tree
The Binary Indexed Tree (BIT) is used to efficiently track the maximum LIS length ending at or before any position:
update(x, v)
: Updates positionx
with valuev
ifv
is greater than the current valuequery(x)
: Returns the maximum value in the range[1, x]
The BIT operations use the bit manipulation trick x & -x
to get the rightmost set bit, which helps navigate the tree structure efficiently.
Step 4: Process Each Index
for x in nums:
v = tree.query(x - 1) + 1
ans = max(ans, v)
tree.update(x, v)
For each index x
in nums
:
- Query the maximum LIS length for all indices less than
x
usingtree.query(x - 1)
- The LIS length ending at
x
is this maximum plus 1 - Update the global maximum LIS length
- Update the BIT at position
x
with this new LIS length
Step 5: Calculate Result
return len(target) - ans
The minimum number of insertions equals the total length of target
minus the length of the longest common subsequence (which is the LIS length we found).
Time Complexity: O(n log m)
where n
is the length of arr
and m
is the length of target
. Each BIT operation takes O(log m)
time.
Space Complexity: O(m)
for the BIT and the dictionary.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with target = [6,4,8,1,3,2]
and arr = [4,7,6,2,3,8,6,1]
.
Step 1: Create Index Mapping
We map each element in target
to its 1-based position:
d = {6:1, 4:2, 8:3, 1:4, 3:5, 2:6}
Step 2: Transform arr to Index Sequence
Convert elements from arr
that exist in target
to their indices:
arr[0] = 4
→ index 2arr[1] = 7
→ not in target, skiparr[2] = 6
→ index 1arr[3] = 2
→ index 6arr[4] = 3
→ index 5arr[5] = 8
→ index 3arr[6] = 6
→ index 1arr[7] = 1
→ index 4
Result: nums = [2, 1, 6, 5, 3, 1, 4]
Step 3 & 4: Find LIS using Binary Indexed Tree
Process each index in nums
to find the longest increasing subsequence:
Index in nums | Value x | query(x-1) | LIS length at x | BIT state after update | ans |
---|---|---|---|---|---|
0 | 2 | query(1)=0 | 0+1=1 | BIT[2]=1 | 1 |
1 | 1 | query(0)=0 | 0+1=1 | BIT[1]=1 | 1 |
2 | 6 | query(5)=1 | 1+1=2 | BIT[6]=2 | 2 |
3 | 5 | query(4)=1 | 1+1=2 | BIT[5]=2 | 2 |
4 | 3 | query(2)=1 | 1+1=2 | BIT[3]=2 | 2 |
5 | 1 | query(0)=0 | 0+1=1 | BIT[1]=1 (no change) | 2 |
6 | 4 | query(3)=2 | 2+1=3 | BIT[4]=3 | 3 |
The longest increasing subsequence has length 3. Looking at our sequence nums = [2, 1, 6, 5, 3, 1, 4]
:
- One valid LIS is
[1, 3, 4]
(positions 1, 4, 6 in nums) - This corresponds to elements
[6, 8, 1]
from the original arrays
Step 5: Calculate Result
Minimum insertions = len(target) - LIS_length = 6 - 3 = 3
We need to insert 3 elements from target
(specifically 4
, 3
, and 2
) to make target
a subsequence of the modified arr
.
Verification: The elements [6, 8, 1]
already appear in arr
in the correct relative order at positions 2, 5, and 7. By inserting the missing elements [4, 3, 2]
at appropriate positions, we can make the entire target
array a subsequence of the modified arr
.
Solution Implementation
1class BinaryIndexedTree:
2 """
3 Binary Indexed Tree (Fenwick Tree) for range maximum queries.
4 This implementation supports point updates and prefix maximum queries.
5 """
6 __slots__ = "size", "tree"
7
8 def __init__(self, n: int):
9 """
10 Initialize a Binary Indexed Tree with size n.
11
12 Args:
13 n: The size of the tree (1-indexed)
14 """
15 self.size = n
16 self.tree = [0] * (n + 1) # 1-indexed array for BIT
17
18 def update(self, index: int, value: int):
19 """
20 Update the tree by setting the maximum value at position index.
21
22 Args:
23 index: The position to update (1-indexed)
24 value: The value to compare with existing maximum
25 """
26 while index <= self.size:
27 # Update current position with maximum value
28 self.tree[index] = max(self.tree[index], value)
29 # Move to next index affected by this update
30 # index & -index gives the lowest set bit
31 index += index & -index
32
33 def query(self, index: int) -> int:
34 """
35 Query the maximum value in range [1, index].
36
37 Args:
38 index: The right boundary of the query range (1-indexed)
39
40 Returns:
41 Maximum value in the range [1, index]
42 """
43 max_value = 0
44 while index > 0:
45 # Take maximum of current position
46 max_value = max(max_value, self.tree[index])
47 # Move to parent node by removing lowest set bit
48 index -= index & -index
49 return max_value
50
51
52class Solution:
53 def minOperations(self, target: List[int], arr: List[int]) -> int:
54 """
55 Find minimum operations to make arr a subsequence of target.
56 This is equivalent to finding (target_length - LCS_length).
57
58 Args:
59 target: The target sequence
60 arr: The array to transform
61
62 Returns:
63 Minimum number of operations needed
64 """
65 # Create position mapping for target elements (1-indexed)
66 position_map = {value: index for index, value in enumerate(target, 1)}
67
68 # Convert arr elements to their positions in target
69 # Filter out elements not in target
70 mapped_positions = [position_map[value] for value in arr if value in position_map]
71
72 # Initialize BIT with size of target
73 target_length = len(target)
74 fenwick_tree = BinaryIndexedTree(target_length)
75
76 # Find longest increasing subsequence in mapped positions
77 # This corresponds to finding the longest common subsequence
78 longest_subsequence = 0
79
80 for position in mapped_positions:
81 # Query maximum length ending before current position
82 prev_max_length = fenwick_tree.query(position - 1)
83 # Current subsequence length including this element
84 current_length = prev_max_length + 1
85 # Update global maximum
86 longest_subsequence = max(longest_subsequence, current_length)
87 # Update tree with new length at this position
88 fenwick_tree.update(position, current_length)
89
90 # Return minimum operations: elements to insert
91 return target_length - longest_subsequence
92
1/**
2 * Binary Indexed Tree (Fenwick Tree) implementation for range maximum queries
3 * This BIT is modified to support maximum value queries instead of sum queries
4 */
5class BinaryIndexedTree {
6 private int size; // Size of the array
7 private int[] tree; // BIT array (1-indexed)
8
9 /**
10 * Constructor to initialize the BIT with given size
11 * @param n Size of the array
12 */
13 public BinaryIndexedTree(int n) {
14 this.size = n;
15 this.tree = new int[n + 1]; // 1-indexed array
16 }
17
18 /**
19 * Updates the BIT by propagating the maximum value upwards
20 * @param index Position to update (1-indexed)
21 * @param value Value to update with (takes maximum)
22 */
23 public void update(int index, int value) {
24 // Traverse all affected nodes in the tree
25 for (; index <= size; index += index & -index) {
26 tree[index] = Math.max(tree[index], value);
27 }
28 }
29
30 /**
31 * Queries the maximum value in range [1, index]
32 * @param index Right boundary of the range (1-indexed)
33 * @return Maximum value in the range
34 */
35 public int query(int index) {
36 int maxValue = 0;
37 // Traverse the tree backwards to get the maximum
38 for (; index > 0; index -= index & -index) {
39 maxValue = Math.max(maxValue, tree[index]);
40 }
41 return maxValue;
42 }
43}
44
45/**
46 * Solution class for finding minimum operations to make target a subsequence of arr
47 * Uses Longest Increasing Subsequence (LIS) approach with Binary Indexed Tree
48 */
49class Solution {
50 /**
51 * Finds minimum operations needed to make target a subsequence of arr
52 * Strategy: Find the longest common subsequence that maintains order,
53 * then remaining elements need to be inserted
54 *
55 * @param target Target array to achieve as subsequence
56 * @param arr Source array to modify
57 * @return Minimum number of insertions needed
58 */
59 public int minOperations(int[] target, int[] arr) {
60 int targetLength = target.length;
61
62 // Map each element in target to its position (1-indexed)
63 Map<Integer, Integer> elementToPosition = new HashMap<>(targetLength);
64 for (int i = 0; i < targetLength; i++) {
65 elementToPosition.put(target[i], i + 1); // 1-indexed position
66 }
67
68 // Extract positions of arr elements that exist in target
69 List<Integer> positions = new ArrayList<>();
70 for (int element : arr) {
71 if (elementToPosition.containsKey(element)) {
72 positions.add(elementToPosition.get(element));
73 }
74 }
75
76 // Find longest increasing subsequence using BIT
77 BinaryIndexedTree fenwickTree = new BinaryIndexedTree(targetLength);
78 int longestIncreasingLength = 0;
79
80 for (int position : positions) {
81 // Query maximum length ending before current position
82 int currentLength = fenwickTree.query(position - 1) + 1;
83 longestIncreasingLength = Math.max(longestIncreasingLength, currentLength);
84 // Update the tree with new length at current position
85 fenwickTree.update(position, currentLength);
86 }
87
88 // Minimum operations = elements to insert = target length - LIS length
89 return targetLength - longestIncreasingLength;
90 }
91}
92
1class BinaryIndexedTree {
2private:
3 int size; // Size of the array
4 vector<int> tree; // Tree array to store maximum values
5
6public:
7 // Constructor: Initialize BIT with given size
8 BinaryIndexedTree(int n) : size(n), tree(n + 1) {}
9
10 // Update position x with maximum value v
11 // Uses BIT update pattern: move to next index by adding lowbit
12 void update(int index, int value) {
13 while (index <= size) {
14 tree[index] = max(tree[index], value);
15 index += index & (-index); // Add lowbit to move to next responsible node
16 }
17 }
18
19 // Query maximum value from indices [1, x]
20 // Uses BIT query pattern: move to parent by subtracting lowbit
21 int query(int index) {
22 int maxValue = 0;
23 while (index > 0) {
24 maxValue = max(maxValue, tree[index]);
25 index -= index & (-index); // Subtract lowbit to move to parent node
26 }
27 return maxValue;
28 }
29};
30
31class Solution {
32public:
33 int minOperations(vector<int>& target, vector<int>& arr) {
34 int targetSize = target.size();
35
36 // Map each target element to its 1-indexed position
37 unordered_map<int, int> targetPositionMap;
38 for (int i = 0; i < targetSize; ++i) {
39 targetPositionMap[target[i]] = i + 1;
40 }
41
42 // Filter arr to only include elements present in target
43 // Store their positions in target array
44 vector<int> filteredPositions;
45 for (int element : arr) {
46 if (targetPositionMap.contains(element)) {
47 filteredPositions.push_back(targetPositionMap[element]);
48 }
49 }
50
51 // Use BIT to find Longest Increasing Subsequence (LIS)
52 BinaryIndexedTree fenwickTree(targetSize);
53 int longestIncreasingSubseq = 0;
54
55 // For each position, find the longest subsequence ending at this position
56 for (int position : filteredPositions) {
57 // Query maximum length of subsequences ending before current position
58 int currentLength = fenwickTree.query(position - 1) + 1;
59 longestIncreasingSubseq = max(longestIncreasingSubseq, currentLength);
60 // Update the tree with the new subsequence length at this position
61 fenwickTree.update(position, currentLength);
62 }
63
64 // Minimum operations = target size - longest common subsequence length
65 return targetSize - longestIncreasingSubseq;
66 }
67};
68
1// Global variables for Binary Indexed Tree (Fenwick Tree)
2let treeSize: number;
3let treeArray: number[];
4
5/**
6 * Initializes the Binary Indexed Tree with given size
7 * @param n - The size of the tree
8 */
9function initializeTree(n: number): void {
10 treeSize = n;
11 // Create array with size n+1 (1-indexed) and initialize with zeros
12 treeArray = Array(n + 1).fill(0);
13}
14
15/**
16 * Updates the Binary Indexed Tree at position x with maximum value v
17 * Uses the lowbit operation (x & -x) to traverse parent nodes
18 * @param x - The position to update (1-indexed)
19 * @param v - The value to update with (maintains maximum)
20 */
21function update(x: number, v: number): void {
22 // Traverse up the tree using lowbit operation
23 for (; x <= treeSize; x += x & -x) {
24 // Update with maximum value
25 treeArray[x] = Math.max(treeArray[x], v);
26 }
27}
28
29/**
30 * Queries the maximum value in range [1, x] from the Binary Indexed Tree
31 * @param x - The upper bound of the query range (1-indexed)
32 * @returns The maximum value in the range
33 */
34function query(x: number): number {
35 let maxValue = 0;
36 // Traverse down the tree using lowbit operation
37 for (; x > 0; x -= x & -x) {
38 // Track the maximum value encountered
39 maxValue = Math.max(maxValue, treeArray[x]);
40 }
41 return maxValue;
42}
43
44/**
45 * Finds minimum operations to make target a subsequence of arr
46 * Uses LCS (Longest Common Subsequence) approach with Binary Indexed Tree optimization
47 * @param target - The target array to achieve
48 * @param arr - The source array to operate on
49 * @returns Minimum number of operations needed
50 */
51function minOperations(target: number[], arr: number[]): number {
52 const targetLength = target.length;
53
54 // Map each target element to its position (1-indexed for BIT)
55 const elementToPosition: Map<number, number> = new Map();
56 target.forEach((element, index) => {
57 elementToPosition.set(element, index + 1);
58 });
59
60 // Extract positions of arr elements that exist in target
61 const mappedPositions: number[] = [];
62 arr.forEach(element => {
63 if (elementToPosition.has(element)) {
64 mappedPositions.push(elementToPosition.get(element)!);
65 }
66 });
67
68 // Initialize Binary Indexed Tree for finding LIS (Longest Increasing Subsequence)
69 initializeTree(targetLength);
70
71 // Find length of longest increasing subsequence
72 let longestIncreasingLength = 0;
73 mappedPositions.forEach(position => {
74 // Query maximum length ending before current position
75 const currentLength = query(position - 1) + 1;
76 // Update global maximum
77 longestIncreasingLength = Math.max(longestIncreasingLength, currentLength);
78 // Update tree with new length at current position
79 update(position, currentLength);
80 });
81
82 // Minimum operations = target length - longest common subsequence length
83 return targetLength - longestIncreasingLength;
84}
85
Time and Space Complexity
Time Complexity: O(n × log m)
The algorithm iterates through the nums
array, which has at most n
elements (where n
is the length of arr
). For each element in nums
:
- The
query
operation on the Binary Indexed Tree takesO(log m)
time, wherem
is the length oftarget
. This is because the query traverses up the tree by removing the lowest set bit (x -= x & -x
), which takes at mostlog m
iterations. - The
update
operation also takesO(log m)
time, as it traverses up the tree by adding the lowest set bit (x += x & -x
), which similarly takes at mostlog m
iterations.
Since we perform both operations for each element in nums
, the total time complexity is O(n × log m)
.
Space Complexity: O(m)
The space usage consists of:
- The dictionary
d
which storesm
key-value pairs (one for each element intarget
):O(m)
- The Binary Indexed Tree's array
c
which has sizem + 1
:O(m)
- The
nums
array which stores at mostn
elements, but sincen
could be less than or equal tom
in the worst case, this doesn't affect the overall complexity - Other variables like
ans
,v
,x
use constant space:O(1)
The dominant space usage is O(m)
, where m
is the length of target
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Duplicate Elements in arr
The Pitfall:
A common mistake is not properly handling duplicates in arr
. When the same element appears multiple times in arr
, each occurrence should be considered independently for building the longest increasing subsequence. Some might incorrectly try to deduplicate arr
or handle duplicates specially, which would lead to wrong answers.
Example:
target = [1, 2, 3]
arr = [1, 1, 2, 2, 3, 3]
Each duplicate can potentially contribute to different valid subsequences. The algorithm correctly processes each occurrence separately through the mapped positions.
Solution: The current implementation handles this correctly by:
mapped_positions = [position_map[value] for value in arr if value in position_map]
This preserves all occurrences of elements from arr
, allowing the LIS algorithm to consider each one.
2. Off-by-One Errors with BIT Indexing
The Pitfall:
Binary Indexed Trees traditionally use 1-based indexing, but mixing 0-based and 1-based indexing can cause subtle bugs. A common error is forgetting to query position - 1
instead of position
:
# WRONG: This would include the current position in the query prev_max_length = fenwick_tree.query(position) # CORRECT: Query strictly less than current position prev_max_length = fenwick_tree.query(position - 1)
Why it matters: We need the maximum LIS length ending at positions strictly less than the current position to maintain the increasing property.
Solution: Always ensure:
- Map target elements to 1-based indices:
enumerate(target, 1)
- Query
position - 1
to get maximum of all previous positions - BIT size should be
len(target)
notlen(target) + 1
since we're already 1-indexed
3. Using Standard LIS Instead of Position-Based LIS
The Pitfall:
Some might try to find the LIS directly on the values in arr
that appear in target
, rather than converting to position indices first:
# WRONG APPROACH: common_elements = [x for x in arr if x in target] lis_length = find_lis(common_elements) # This won't work!
Why this fails:
The LIS of actual values doesn't correspond to the longest common subsequence. We need to respect the order of elements as they appear in target
.
Example:
target = [5, 1, 3]
arr = [1, 3, 5]
Direct LIS on [1, 3, 5]
gives length 3, but the actual LCS is length 0 because the order in arr
doesn't match target
's order.
Solution:
Transform to position indices first, then find LIS on those positions. This ensures we're finding subsequences that respect target
's ordering.
4. Memory Optimization Confusion
The Pitfall: Some might try to optimize memory by using a smaller BIT size based on the number of unique elements actually present:
# WRONG: Using only the size of common elements
unique_common = len(set(arr) & set(target))
fenwick_tree = BinaryIndexedTree(unique_common)
Why this fails:
The position indices can range from 1 to len(target)
, regardless of how many elements from target
actually appear in arr
. Using a smaller BIT would cause index out of bounds errors.
Solution:
Always initialize the BIT with the full size of target
:
fenwick_tree = BinaryIndexedTree(len(target))
Which of these properties could exist for a graph but not a tree?
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
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
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!