Skip to content
advancedPhase 6 · Dynamic Programming

Longest Increasing Subsequence

Master LIS and its variants for sequence optimization problems.

1h
5 problems
Topic Progress0%

LIS with Dynamic Programming

The Longest Increasing Subsequence (LIS) problem: Find the length of the longest subsequence where elements are in strictly increasing order.

Definition

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of remaining elements.

DP Approach: O(n^2)

// dp[i] = length of LIS ending at index i
public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);  // Each element is LIS of length 1
    
    int maxLength = 1;
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        maxLength = Math.max(maxLength, dp[i]);
    }
    
    return maxLength;
}

Trace Example

nums = [10, 9, 2, 5, 3, 7, 101, 18]

dp[0] = 1  (10)
dp[1] = 1  (9, no previous smaller)
dp[2] = 1  (2, no previous smaller)
dp[3] = 2  (2 < 5, extend from dp[2])
dp[4] = 2  (2 < 3, extend from dp[2])
dp[5] = 3  (2 < 3 < 7, extend from dp[4])
dp[6] = 4  (3 < 7 < 101, extend from dp[5])
dp[7] = 4  (3 < 7 < 18, extend from dp[5])

Answer: 4 (subsequence: [2, 3, 7, 101] or [2, 3, 7, 18])

Reconstructing the LIS

public List<Integer> reconstructLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    int[] parent = new int[n];  // Track previous index in LIS
    Arrays.fill(dp, 1);
    Arrays.fill(parent, -1);
    
    int maxLength = 1;
    int maxIndex = 0;
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
                parent[i] = j;  // Record predecessor
            }
        }
        if (dp[i] > maxLength) {
            maxLength = dp[i];
            maxIndex = i;
        }
    }
    
    // Reconstruct path
    List<Integer> lis = new ArrayList<>();
    int curr = maxIndex;
    while (curr != -1) {
        lis.add(0, nums[curr]);
        curr = parent[curr];
    }
    
    return lis;
}

Time and Space Complexity

  • Time: O(n^2) - two nested loops
  • Space: O(n) - dp and parent arrays

When O(n^2) is Acceptable

  • n <= 1000
  • Need to reconstruct the actual LIS
  • Need to find all LIS of maximum length

O(n log n) Optimization with Binary Search

Patience Sorting Approach

Maintain an array tails where tails[i] is the smallest tail element for increasing subsequences of length i+1.

// O(n log n) time, O(n) space
public int lengthOfLIS(int[] nums) {
    List<Integer> tails = new ArrayList<>();
    
    for (int num : nums) {
        // Find position where num should be placed
        int pos = Collections.binarySearch(tails, num);
        
        if (pos < 0) {
            pos = -(pos + 1);  // Insertion point
        }
        
        if (pos == tails.size()) {
            tails.add(num);  // Extend longest subsequence
        } else {
            tails.set(pos, num);  // Replace to keep smallest tail
        }
    }
    
    return tails.size();
}

Why It Works

nums = [10, 9, 2, 5, 3, 7, 101, 18]

num=10:  tails = [10]
num=9:   tails = [9]       (replace 10)
num=2:   tails = [2]       (replace 9)
num=5:   tails = [2, 5]    (extend)
num=3:   tails = [2, 3]    (replace 5)
num=7:   tails = [2, 3, 7] (extend)
num=101: tails = [2, 3, 7, 101] (extend)
num=18:  tails = [2, 3, 7, 18]  (replace 101)

Answer: tails.size() = 4

Important Note

The tails array is NOT the LIS itself! It only gives the length. To reconstruct, you need additional tracking.

Reconstructing LIS with O(n log n)

public List<Integer> reconstructLIS(int[] nums) {
    int n = nums.length;
    List<Integer> tails = new ArrayList<>();
    int[] tailIndices = new int[n];  // Index in tails array
    int[] parent = new int[n];       // Previous index in LIS
    Arrays.fill(parent, -1);
    
    for (int i = 0; i < n; i++) {
        int pos = Collections.binarySearch(tails, nums[i]);
        if (pos < 0) pos = -(pos + 1);
        
        if (pos == tails.size()) {
            tails.add(nums[i]);
        } else {
            tails.set(pos, nums[i]);
        }
        
        tailIndices[i] = pos;
        if (pos > 0) {
            // Find the last element with tailIndices = pos - 1
            for (int j = i - 1; j >= 0; j--) {
                if (tailIndices[j] == pos - 1) {
                    parent[i] = j;
                    break;
                }
            }
        }
    }
    
    // Reconstruct
    int lisLen = tails.size();
    List<Integer> lis = new ArrayList<>();
    int curr = -1;
    for (int i = n - 1; i >= 0; i--) {
        if (tailIndices[i] == lisLen - 1) {
            curr = i;
            break;
        }
    }
    
    while (curr != -1) {
        lis.add(0, nums[curr]);
        curr = parent[curr];
    }
    
    return lis;
}

LIS Variants

Longest Decreasing Subsequence:

// Negate all numbers, then find LIS
public int LDS(int[] nums) {
    int[] negated = new int[nums.length];
    for (int i = 0; i < nums.length; i++) {
        negated[i] = -nums[i];
    }
    return lengthOfLIS(negated);
}

Longest Bitonic Subsequence:

// Find LIS from left and LDS from right
public int longestBitonic(int[] nums) {
    int n = nums.length;
    int[] lis = new int[n];
    int[] lds = new int[n];
    
    // LIS from left
    for (int i = 0; i < n; i++) {
        lis[i] = 1;
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                lis[i] = Math.max(lis[i], lis[j] + 1);
            }
        }
    }
    
    // LDS from right
    for (int i = n - 1; i >= 0; i--) {
        lds[i] = 1;
        for (int j = n - 1; j > i; j--) {
            if (nums[j] < nums[i]) {
                lds[i] = Math.max(lds[i], lds[j] + 1);
            }
        }
    }
    
    int maxLen = 0;
    for (int i = 0; i < n; i++) {
        maxLen = Math.max(maxLen, lis[i] + lds[i] - 1);
    }
    
    return maxLen;
}

Complexity Comparison

Approach Time Space Reconstruct?
DP O(n^2) O(n^2) O(n) Yes
Binary Search O(n log n) O(n) Yes (with extra work)

Practice Problems

0/1solved
Longest Increasing Subsequence
DP + Binary Search

Given an integer array nums, return the length of the longest strictly increasing subsequence.

Example:

Input: nums = [10,9,2,5,3,7,101,18]

Output: 4

The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Optimal Solution — O(n log n) time, O(n) space

O(n log n) solution using patience sorting with binary search. Maintain tails array where tails[i] is smallest tail of all increasing subsequences of length i+1.

class Solution {
    public int lengthOfLIS(int[] nums) {
        List<Integer> tails = new ArrayList<>();
        
        for (int num : nums) {
            int pos = Collections.binarySearch(tails, num);
            if (pos < 0) {
                pos = -(pos + 1);
            }
            
            if (pos == tails.size()) {
                tails.add(num);
            } else {
                tails.set(pos, num);
            }
        }
        
        return tails.size();
    }
}

Edge Cases:

  • All elements equal: LIS length = 1
  • Already sorted: LIS length = n
  • Reverse sorted: LIS length = 1
  • Single element: LIS length = 1
  • Negative numbers included

Quiz

1. In the O(n log n) LIS solution, what does the tails array represent?

Question 1 options

2. Why do we use binary search in the optimized LIS solution?

Question 2 options

3. What is the primary purpose of Longest Increasing Subsequence?

Question 3 options

4. What is a common mistake when implementing Longest Increasing Subsequence?

Question 4 options

Flashcards

Question

What is the time complexity of LIS using DP vs binary search?

Answer

DP: O(n^2) using nested loops. Binary Search: O(n log n) using patience sorting with tails array.

Question

Does the tails array in O(n log n) LIS contain the actual LIS?

Answer

No! The tails array only gives the length of LIS. It maintains smallest tails for each possible length, but the order may not reflect the actual LIS.

Question

What is Longest Increasing Subsequence?

Answer

Longest Increasing Subsequence is a key concept in software engineering.

Question

When to use Longest Increasing Subsequence?

Answer

Use Longest Increasing Subsequence when building production systems that require reliability, scalability, and maintainability.

Question

Longest Increasing Subsequence best practices

Answer

Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.

Revision Notes

Key Takeaways

  • 1.dp[i] = LIS length ending at index i (O(n^2) approach)
  • 2.tails array maintains smallest tails for each length (O(n log n))
  • 3.tails array does NOT contain the actual LIS
  • 4.Binary search finds correct position in O(log n)

Interview Tips

  • Start with O(n^2) DP, then optimize to O(n log n)
  • Clarify if you need length or actual subsequence
  • Explain why tails array works (invariant property)
  • Discuss when to use each approach

Cheat Sheet

LIS Cheat Sheet

O(n^2) DP:

  • dp[i] = LIS length ending at index i
  • dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i]
  • Answer = max(dp[i])

O(n log n) Binary Search:

  • tails[i] = smallest tail for LIS of length i+1
  • For each num, binary search position in tails
  • If pos == tails.size(): extend (add to tails)
  • Else: replace tails[pos] with num
  • Answer = tails.size()

Reconstruction:

  • Track parent pointers in O(n^2) approach
  • In O(n log n), track tailIndices and parent arrays

Variants:

  • LDS: negate all numbers, find LIS
  • Longest Bitonic: LIS from left + LDS from right
  • Print LIS: use parent array for backtracking