Longest Increasing Subsequence

Hero Image

DT

Dhaval Trivedi

Co-founder, Airtribe

Problem Overview

The problem at hand is the "Longest Increasing Subsequence." Your task is to find the length of the longest subsequence of a given sequence where the subsequence’s elements are in sorted order, lowest to highest, and are not necessarily contiguous. This is a classic dynamic programming problem that focuses on manipulating sequences of numbers efficiently.

Understanding the Problem

A subsequence is a derived sequence obtained by deleting some or no elements of the array without changing the order of the remaining elements. The "Longest Increasing Subsequence" (LIS) problem requires us to identify the longest subsequence where each element is strictly greater than the previous one.

Example: Given the array nums = [10, 9, 2, 5, 3, 7, 101, 18], the LIS is [2, 3, 7, 101] which has a length of 4.

Key DSA Concepts and Theory

Dynamic Programming:

Dynamic programming is a method for solving complex problems by breaking them down into simpler subproblems. It is applicable to problems that have overlapping subproblems and optimal substructure.

Optimal Substructure: A problem exhibits optimal substructure if an optimal solution can be constructed from optimal solutions of its subproblems.

Overlapping Subproblems: The problem can be broken down into subproblems that are reused several times.

For the LIS problem, dynamic programming can be applied to store intermediate results which help in building the solution incrementally.

Binary search is a search algorithm that finds the position of a target value within a sorted array. It is used in an efficient optimization of the LIS problem where we want to improve time complexity by reducing the search space.

Solution Approach

Dynamic Programming Approach:

  1. Initialize an Array: Create an array dp[] where dp[i] holds the length of the longest increasing subsequence ending at index i. Initialize all entries of dp[] as 1 since the minimum LIS ending with each element is the element itself.

  2. Iterate over Elements: For each element nums[i], check all elements nums[j] before it. If nums[i] is larger than nums[j], update dp[i] if dp[j] + 1 is greater than dp[i].

  3. Result Evaluation: The length of the longest increasing subsequence is the maximum value in the dp[] array.

C++ Code:

#include <vector>
#include <algorithm>

class Solution {
public:
    int lengthOfLIS(std::vector<int>& nums) {
        if (nums.empty()) return 0; 
        int n = nums.size();
        std::vector<int> dp(n, 1); 

        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = std::max(dp[i], dp[j] + 1);
                }
            }
        }
        return *std::max_element(dp.begin(), dp.end());
    }
};

Binary Search Optimized Approach:

  1. Use List to Track Ends: Use a list lis to track the end elements of potential increasing subsequences.

  2. Traverse the Array: For each element num, use binary search to find its position pos in the lis.

  3. Replace or Extend LIS: If pos equals the size of the list, append num to lis; else, replace lis[pos] with num.

  4. Result Evaluation: The size of the list lis gives the length of the LIS.

Java Code:

import java.util.ArrayList;
import java.util.List;

class Solution {
    public int lengthOfLIS(int[] nums) {
        List<Integer> lis = new ArrayList<>();

        for (int num : nums) {
            int pos = binarySearch(lis, num);
            if (pos < lis.size()) {
                lis.set(pos, num);
            } else {
                lis.add(num);
            }
        }
        return lis.size();
    }

    private int binarySearch(List<Integer> lis, int target) {
        int low = 0, high = lis.size() - 1;
        while (low <= high) {
            int mid = low + (high - low) / 2;
            if (lis.get(mid) < target) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        return low;
    }
}

Time and Space Complexity Analysis

Dynamic Programming Approach:

  • Time Complexity: O(n^2), where n is the length of the input array. This is because of the nested iteration over the array elements.
  • Space Complexity: O(n) because of the dp array.

Binary Search Optimized Approach:

  • Time Complexity: O(n log n), due to binary search operations within the list which track the end of increasing subsequences.
  • Space Complexity: O(n), as space is required to store the lis list.

Common Mistakes to Avoid

  • Failing to update dp[i] properly when a longer subsequence is found.
  • Confusing subsequences with substrings or requiring the subsequence elements to be contiguous.

Similar Problems on LeetCode

  • Longest Common Subsequence - LeetCode 1143
  • Increasing Triplet Subsequence - LeetCode 334
  • Longest Increasing Path in a Matrix - LeetCode 329

Additional Resources and References

This article aims to give a thorough understanding of solving the Longest Increasing Subsequence problem using dynamic programming and binary search techniques, equipping you with both a fundamental and optimized solution to tackle similar questions efficiently.