Longest Increasing Subsequence (LIS)

The longest increasing subsequence problem asks for the maximum length of a strictly increasing subsequence in an array. This lesson covers the classic O(n²) DP (LeetCode 300), the optimized O(n log n) patience-sorting approach, and full solutions in C, Java, and Python with output trace tables.

Overview

Given an integer array nums, find the length of the longest strictly increasing subsequence. A subsequence keeps relative order but may skip elements.

  • Subsequence — not necessarily contiguous (e.g. [2, 3, 7, 101] from the array below)
  • Strictly increasing — each next element must be greater than the previous
  • Two standard solutions — O(n²) DP for clarity; O(n log n) with binary search for interviews
Problem Goal State LeetCode
LIS lengthMax lengthdp[i] = LIS ending at i300
Number of LISCount distinct LIS of max lengthdp[i] + count[i]673
Russian doll envelopes2D LIS (width × height)Sort + LIS on one dimension354
LIS reconstructionReturn one actual subsequenceParent pointers from DPFollow-up to 300
Example: nums = [10, 9, 2, 5, 3, 7, 101, 18]
Index:         0   1  2  3  4  5   6    7

One LIS of length 4:  2 → 3 → 7 → 101   (indices 2, 4, 5, 6)
Another:              2 → 5 → 7 → 101   (indices 2, 3, 5, 6)
Answer (length only): 4

LIS — O(n²) Dynamic Programming

State: dp[i] = length of the longest increasing subsequence that ends at index i.

Recurrence: For each j < i where nums[j] < nums[i], update dp[i] = max(dp[i], dp[j] + 1).

Base: Every single element is a subsequence of length 1 → initialize dp[i] = 1.

Answer: max(dp) over all indices.

Why this works: Optimal substructure — any LIS ending at i extends a LIS ending at some earlier j with a smaller value. Overlapping subproblems appear when many i share the same best predecessor j.

O(n²) LIS Code (C, Java, Python)

Keywords Types Functions Variables Strings Numbers Comments

Python

def length_of_lis(nums):
    """LeetCode 300 — O(n²) DP. dp[i] = LIS length ending at index i."""
    n = len(nums)
    if n == 0:
        return 0

    dp = [1] * n  # each element alone is length 1
    best = 1

    for i in range(1, n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
        best = max(best, dp[i])

    return best


if __name__ == "__main__":
    tests = [
        [10, 9, 2, 5, 3, 7, 101, 18],
        [0, 1, 0, 3, 2, 3],
        [7, 7, 7, 7],
    ]
    for nums in tests:
        print(f"nums={nums} → LIS length = {length_of_lis(nums)}")

Java

public class LisOn2 {

    /** O(n²) LIS — bottom-up DP ending at each index. */
    public static int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;

        int[] dp = new int[n];
        int best = 1;

        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            best = Math.max(best, dp[i]);
        }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(lengthOfLIS(new int[]{10, 9, 2, 5, 3, 7, 101, 18})); // 4
        System.out.println(lengthOfLIS(new int[]{0, 1, 0, 3, 2, 3}));             // 4
        System.out.println(lengthOfLIS(new int[]{7, 7, 7, 7}));                   // 1
    }
}

C

#include <stdio.h>

#define MAX_N 10000

/* O(n²) LIS length */
int lis_on2(int nums[], int n) {
    int dp[MAX_N];
    int i, j, best = 1;

    if (n == 0) return 0;

    for (i = 0; i < n; i++) {
        dp[i] = 1;
        for (j = 0; j < i; j++) {
            if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
            }
        }
        if (dp[i] > best) best = dp[i];
    }
    return best;
}

int main(void) {
    int a[] = {10, 9, 2, 5, 3, 7, 101, 18};
    int b[] = {0, 1, 0, 3, 2, 3};
    int c[] = {7, 7, 7, 7};
    printf("%d\n", lis_on2(a, 8)); /* 4 */
    printf("%d\n", lis_on2(b, 6)); /* 4 */
    printf("%d\n", lis_on2(c, 4)); /* 1 */
    return 0;
}

O(n²) Output & Trace

Program output (all three languages)

Language Input Console Output Explanation
Python[10,9,2,5,3,7,101,18]→ 4e.g. 2, 3, 7, 101
Python[0,1,0,3,2,3]→ 40, 1, 2, 3
Python[7,7,7,7]→ 1Strict increase — no pair works
Java / Csame tests4
4
1
Identical logic; C prints one per line

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

Index i nums[i] dp[i] Best predecessor j Explanation
0101Base: [10]
1919 < 10, no extension
221Smallest so far
352j=2 (2)2 → 5
432j=2 (2)2 → 3
573j=4 (3)2 → 3 → 7
61014j=5 (7)2 → 3 → 7 → 101
7184j=5 (7)2 → 5 → 7 → 18 also length 4

LIS — O(n log n) Patience Sorting

The optimized approach maintains an array tails where tails[k] is the smallest possible tail value of an increasing subsequence of length k + 1.

For each num in nums:

  1. Binary search tails for the leftmost index where tails[i] ≥ num
  2. If found, replace tails[i] = num (improve the tail for that length)
  3. If not found, append num (extend LIS by one)

Final answer = len(tails). Time O(n log n), space O(n).

Important: tails is not the actual LIS — it stores candidate tail values for each length. Use parent-pointer DP if you must reconstruct the sequence.

O(n log n) LIS Code (C, Java, Python)

Python

def length_of_lis_nlogn(nums):
    """LeetCode 300 — O(n log n) via patience sorting + binary search."""
    tails = []

    for num in nums:
        left, right = 0, len(tails)
        # find first index with tails[i] >= num
        while left < right:
            mid = (left + right) // 2
            if tails[mid] < num:
                left = mid + 1
            else:
                right = mid

        if left == len(tails):
            tails.append(num)       # extend LIS length
        else:
            tails[left] = num       # replace smaller tail

    return len(tails)


if __name__ == "__main__":
    tests = [
        [10, 9, 2, 5, 3, 7, 101, 18],
        [0, 1, 0, 3, 2, 3],
        [7, 7, 7, 7],
    ]
    for nums in tests:
        print(f"nums={nums} → LIS length = {length_of_lis_nlogn(nums)}")

Java

public class LisNLogN {

    /** O(n log n) LIS using tails array and binary search. */
    public static int lengthOfLIS(int[] nums) {
        int[] tails = new int[nums.length];
        int size = 0;

        for (int num : nums) {
            int left = 0, right = size;
            while (left < right) {
                int mid = (left + right) / 2;
                if (tails[mid] < num) left = mid + 1;
                else right = mid;
            }
            if (left == size) {
                tails[size++] = num;
            } else {
                tails[left] = num;
            }
        }
        return size;
    }

    public static void main(String[] args) {
        System.out.println(lengthOfLIS(new int[]{10, 9, 2, 5, 3, 7, 101, 18})); // 4
        System.out.println(lengthOfLIS(new int[]{0, 1, 0, 3, 2, 3}));             // 4
        System.out.println(lengthOfLIS(new int[]{7, 7, 7, 7}));                   // 1
    }
}

C

#include <stdio.h>

#define MAX_N 10000

/* Binary search: first index in tails[0..size-1] where tails[i] >= num */
int lower_bound(int tails[], int size, int num) {
    int left = 0, right = size;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (tails[mid] < num) left = mid + 1;
        else right = mid;
    }
    return left;
}

int lis_nlogn(int nums[], int n) {
    int tails[MAX_N];
    int size = 0, i, pos;

    for (i = 0; i < n; i++) {
        pos = lower_bound(tails, size, nums[i]);
        if (pos == size) tails[size++] = nums[i];
        else tails[pos] = nums[i];
    }
    return size;
}

int main(void) {
    int a[] = {10, 9, 2, 5, 3, 7, 101, 18};
    int b[] = {0, 1, 0, 3, 2, 3};
    int c[] = {7, 7, 7, 7};
    printf("%d\n", lis_nlogn(a, 8)); /* 4 */
    printf("%d\n", lis_nlogn(b, 6)); /* 4 */
    printf("%d\n", lis_nlogn(c, 4)); /* 1 */
    return 0;
}

O(n log n) Output & Trace

Program output

Language Input Output Notes
Python[10,9,2,5,3,7,101,18]4Same answer as O(n²)
Python[0,1,0,3,2,3]40 → 1 → 2 → 3
Python[7,7,7,7]1Strict — duplicates block growth
Java / Csame tests4, 4, 1Identical results

Tails array evolution — nums = [10, 9, 2, 5, 3, 7, 101, 18]

Process num Action tails after step LIS length so far
10append[10]1
9replace index 0[9]1
2replace index 0[2]1
5append[2, 5]2
3replace index 1[2, 3]2
7append[2, 3, 7]3
101append[2, 3, 7, 101]4
18replace index 3[2, 3, 7, 18]4 (final)
Patience sorting analogy: Each card (number) goes on the leftmost pile whose top is ≥ it. The number of piles equals the LIS length — a classic card-game proof of correctness.

LIS Variants & Related Problems

Variant Twist Approach LeetCode
Non-decreasing LISAllow equal valuesChange < to <= in DPCustom
Number of LISCount max-length sequencesTrack count[i] alongside dp[i]673
Russian doll envelopes2D: width and height both increaseSort by width asc, height desc; LIS on heights354
Longest bitonicIncrease then decreaseLIS forward + LIS backwardCustom / interview
LIS reconstructionReturn actual subsequenceStore parent[i] in O(n²) DPFollow-up

Connect to subsequence problems for LCS and related string DP, and to knapsack when selection under constraints appears.

More Examples

Example 1: All decreasing

numsO(n²) dpLIS lengthWhy
[5, 4, 3, 2, 1][1,1,1,1,1]1No pair satisfies nums[j] < nums[i]

Example 2: Already sorted

numsLIS lengthOne LISComplexity note
[1, 2, 3, 4, 5]5Entire arrayBest case — every j extends i

Example 3: Complexity summary

ApproachTimeSpaceNotes
O(n²) DPO(n²)O(n)Easy to explain; supports reconstruction
O(n log n) tailsO(n log n)O(n)Interview optimal for length only
Brute force subsetsO(2ⁿ)O(n)Never use in interviews

Approach Comparison

Approach Time Space When to use
O(n²) DPO(n²)O(n)Whiteboard first draft; need parent pointers
O(n log n) + binary searchO(n log n)O(n)Final answer when only length is required
O(n²) + count arrayO(n²)O(n)Number of LIS (LeetCode 673)
Sort + LISO(n log n)O(n)2D problems like envelopes (354)

Real-Life Applications

LIS models finding the longest monotonically improving pattern in ordered data — trends, rankings, and timelines where each next pick must beat the previous.

Domain Real scenario LIS mapping Variant
FinanceLongest streak of days with rising closing pricesDaily prices = nums; subsequence = non-contiguous up-daysStandard LIS
Sports analyticsBest improving performance sequence across seasonsSeason stats sorted by time; LIS on scoresLength or reconstruction
ManufacturingScheduling jobs with increasing priority levelsJob priorities = nums; pick subsequence respecting orderNon-decreasing variant
BioinformaticsLongest chain of increasing gene expression valuesExpression time series → LIS on measurementsO(n log n) for long sequences
LogisticsNesting boxes (Russian doll) — fit smaller inside largerWidth × height pairs; sort + LIS on second dimension2D LIS (354)
UI / UXLongest strictly increasing user engagement scoresSession metrics over timeStandard LIS
Patience card gameMinimum piles to sort a shuffled deckDirect patience-sorting analogyO(n log n) tails
Version controlLongest chain of compatible library upgradesVersion numbers as nums (after normalization)Strict LIS on semver tuples
Pattern recognition: Whenever a problem asks for “longest chain where each next element is greater” in original order — think LIS. For 2D “fit inside” problems, sort one dimension and run LIS on the other.

Interview Tips

  1. Clarify: strictly increasing or non-decreasing? Return length or actual subsequence?
  2. Start with O(n²): define dp[i] as LIS ending at i, base case 1.
  3. Trace a small array (4–5 elements) on the whiteboard before coding.
  4. Offer O(n log n) with tails + binary search as the optimized solution.
  5. Explain that tails is not the LIS itself — only length unless you add parent tracking.
  6. For envelopes (354): sort by width ascending, height descending, then LIS on heights.
  7. State complexities: O(n²) time O(n) space vs O(n log n) time O(n) space.
  8. Connect to LCS / subsequence DP when strings replace arrays.

Key Takeaway

LIS uses dp[i] = longest increasing subsequence ending at i (O(n²)), or a tails array with binary search (O(n log n)) when only length matters. Master both — start with DP for clarity, finish with patience sorting for optimal complexity.

Next up: Travel & candies · Subsequence problems · Knapsack problems