Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 in target), their corresponding indices in nums 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 position x with value v if v is greater than the current value
  • query(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:

  1. Query the maximum LIS length for all indices less than x using tree.query(x - 1)
  2. The LIS length ending at x is this maximum plus 1
  3. Update the global maximum LIS length
  4. 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 Evaluator

Example 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 2
  • arr[1] = 7 → not in target, skip
  • arr[2] = 6 → index 1
  • arr[3] = 2 → index 6
  • arr[4] = 3 → index 5
  • arr[5] = 8 → index 3
  • arr[6] = 6 → index 1
  • arr[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 numsValue xquery(x-1)LIS length at xBIT state after updateans
02query(1)=00+1=1BIT[2]=11
11query(0)=00+1=1BIT[1]=11
26query(5)=11+1=2BIT[6]=22
35query(4)=11+1=2BIT[5]=22
43query(2)=11+1=2BIT[3]=22
51query(0)=00+1=1BIT[1]=1 (no change)2
64query(3)=22+1=3BIT[4]=33

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 takes O(log m) time, where m is the length of target. This is because the query traverses up the tree by removing the lowest set bit (x -= x & -x), which takes at most log m iterations.
  • The update operation also takes O(log m) time, as it traverses up the tree by adding the lowest set bit (x += x & -x), which similarly takes at most log 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 stores m key-value pairs (one for each element in target): O(m)
  • The Binary Indexed Tree's array c which has size m + 1: O(m)
  • The nums array which stores at most n elements, but since n could be less than or equal to m 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) not len(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))
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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

Load More