Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.19 Longest Increasing Subsequence

Source: src/main/kotlin/array/dp/LongestIncreasingSubsequence.kt (+ tree/bst/LongestIncreasingSubsequence.kt — a TreeSet variant) Pattern: dp[i] = best ending at i · Core page

The Problem

Given nums, return the length of the longest strictly increasing subsequence (not necessarily contiguous).

  • Constraints: $1 \le n \le 2500$; values fit in Int.

Examples

Input:  nums = [10,9,2,5,3,7,101,18]   -> Output: 4   ([2,3,7,101])
Input:  nums = [0,1,0,3,2,3]           -> Output: 4   ([0,1,2,3])

Intuition — dp[i] = the longest increasing subsequence ending at i

The contiguous cousin (2.18) needs only the previous answer; LIS needs every previous index because an increasing subsequence may skip elements:

$$ dp[i] = 1 + \max_{j < i,; nums[j] < nums[i]} dp[j] $$

Why scan all j < i? The subsequence ending at i can extend any earlier subsequence whose last value is smaller — the best one isn’t necessarily the immediately previous index. That’s the O(n²) loop: for each i, scan j in 0..i-1, and if nums[j] < nums[i], consider dp[j] + 1.

The dp init to 1 — every element alone is a valid increasing subsequence of length 1; the maxOrNull() ?: 1 on the return handles the size-1 array.

The O(n log n) upgrade — a “tails” array + binary search (1.0’s theorem): tails[k] = the smallest tail of an increasing subsequence of length k. The TreeSet variant (tree/bst/LongestIncreasingSubsequence.kt) implements the same idea with a balanced tree. Mention it in the interview; implement the O(n²) first.

Approach 1 — LCS trick (O(n^2) with more machinery)

Sort a copy and take the LCS with the original — correct but with duplicates issues; the direct DP is cleaner.

Approach 2 — dp[i] over all previous (the repo’s version)

class LongestIncreasingSubsequence {
    /**
     * @param nums input array
     * @return     length of the longest strictly increasing subsequence
     */
    fun lengthOfLIS(nums: IntArray): Int {
        if (nums.isEmpty()) return 0

        val dp = IntArray(nums.size) { 1 }     // every element is length-1 by itself

        for (i in 1 until nums.size) {
            for (j in 0 until i) {
                if (nums[i] > nums[j]) {       // can extend the subsequence ending at j
                    dp[i] = maxOf(dp[i], dp[j] + 1)
                }
            }
        }
        return dp.maxOrNull() ?: 1
    }
}
public class LongestIncreasingSubsequence {
    /**
     * @param nums input array
     * @return     length of the longest strictly increasing subsequence
     */
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);                        // every element is length-1 by itself
        int best = 1;

        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            best = Math.max(best, dp[i]);
        }
        return best;
    }
}
#include <vector>
#include <algorithm>

class LongestIncreasingSubsequence {
public:
    /**
     * @param nums input array
     * @return     length of the longest strictly increasing subsequence
     */
    int lengthOfLIS(std::vector<int>& nums) {
        std::vector<int> dp(nums.size(), 1);       // every element is length-1 by itself
        int best = 1;

        for (int i = 1; i < (int)nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) dp[i] = std::max(dp[i], dp[j] + 1);
            }
            best = std::max(best, dp[i]);
        }
        return best;
    }
};
def length_of_lis(nums: list[int]) -> int:
    """
    @param nums: input array
    @return:     length of the longest strictly increasing subsequence
    """
    dp = [1] * len(nums)             # every element is length-1 by itself

    for i in range(1, len(nums)):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
#![allow(unused)]
fn main() {
impl Solution {
    /// @param nums input array
    /// @return     length of the longest strictly increasing subsequence
    pub fn length_of_lis(nums: Vec<i32>) -> i32 {
        let mut dp = vec![1; nums.len()];    // every element is length-1 by itself

        for i in 1..nums.len() {
            for j in 0..i {
                if nums[i] > nums[j] {
                    dp[i] = dp[i].max(dp[j] + 1);
                }
            }
        }
        dp.into_iter().max().unwrap()
    }
}
}

Dry run

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

dp = [1,1,1,1,1,1,1,1]

i=1 (9):  j=0: 9 > 10? no -> dp[1]=1
i=2 (2):  j=0,1: 2 > 10/9? no -> dp[2]=1
i=3 (5):  j=2: 5 > 2 -> dp[3]=2
i=4 (3):  j=2: 3 > 2 -> dp[4]=2   (j=3: 3 > 5? no)
i=5 (7):  j=2: 7>2 -> 2;  j=3: 7>5 -> dp[3]+1=3;  j=4: 7>3 -> dp[4]+1=3.  dp[5]=3
i=6 (101): best extension = dp[5]+1 = 4.  dp[6]=4
i=7 (18): j=5: 18>7 -> dp[5]+1=4.  dp[7]=4

max(dp) = 4 ✓   ([2,3,7,101] or [2,5,7,101] or [2,3,7,18])

The all-previous scan is what makes it non-contiguous: at i=5 (7), the best extension is dp[3] (ending at 5, from [2,5]) or dp[4] (ending at 3, from [2,3]) — not the immediately-previous index. Every j < i with a smaller value is a candidate chain.

Complexity

Time. All pairs:

$$ T(n) = O(n^2) $$

Space. The dp array:

$$ S(n) = O(n) $$

Variants & follow-ups

  • Russian Doll Envelopes (14.6) — LIS in 2-D: sort by one dimension, LIS on the other.
  • Minimum Number Of Removals To Make Mountain Array (tree/bst/MinimumNumberOfRemovalsToMakeMountainArray.kt) — LIS + LDS around a peak.
  • Longest Increasing Sequence In A Matrix (array/dp/LongestIncreasingSequenceInAMatrix.kt) — the grid version: DFS + memo.
  • Interview follow-up: “Can you do O(n log n)?” Yes — the tails array: tails[k] = smallest tail of a length-k subsequence; binary search the insertion point (1.0). Same length, different bookkeeping; the TreeSet repo variant is that idea in a balanced tree.