Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 24 – Dynamic programming: foundations

Longest increasing subsequence

The longest increasing subsequence, set as months of improving test scores: the problem and its limits, a brute force over every choice, and what to notice.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25.0.4.1, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 7 of 35 minutes free
Free: Improving months · The brute force, and why it fails at these sizes · What to notice. With a pass: the other 12 sections and 3 interview answers, about 28 minutes.

What you will learn

  • Compute the longest increasing subsequence in O(n²) with a table of run lengths
  • Compute its length in O(n log n) from the smallest last value of each length
  • Prove that the list of smallest last values stays sorted and ends with the right length
  • Rebuild one longest subsequence from parent indices
  • Choose the right binary search for strictly rising and for never-falling runs

Before you start

What you’ll learn · Before you start

What you will learn

  • Compute the longest increasing subsequence in O(n²) with a table of run lengths
  • Compute its length in O(n log n) from the smallest last value of each length
  • Prove that the list of smallest last values stays sorted and ends with the right length
  • Rebuild one longest subsequence from parent indices
  • Choose the right binary search for strictly rising and for never-falling runs

Before you start

On this page

The longest increasing subsequence (LIS) of a list is the longest selection of its values, kept in their original order but not necessarily next to each other, in which every value is larger than the one before. In [52, 48, 61, 55, 70, 66, 74, 59] it has length 4: 52, 61, 70 and 74 is one answer, and 48, 55, 66 and 74 is another. It is a classic interview problem because a correct answer is easy to write and slow, and a fast one needs a single observation about which runs are worth keeping. The same idea returns in nesting boxes, chains of pairs and the fewest changes that make a list rise.

Improving months

The problem. A learner sits one mock test a month and writes down the score, scores[0..n − 1]. Choose some of the months, keeping their order, so that every chosen score is strictly higher than the one chosen before it. Return the largest number of months you can choose, and one such list of scores. Limits: 1 ≤ n ≤ 2 × 10⁵ and 0 ≤ scores[i] ≤ 10⁹.

  • [52, 48, 61, 55, 70, 66, 74, 59] gives 4, for example with 52, 61, 70 and 74.
  • [40, 40, 40] gives 1, because an equal score is not an improvement.
  • [90, 80, 70] gives 1, because each month on its own is a run of one.
Eight monthly scores plotted by month, with two different improving runs of four months drawn through them.Monthly scores: two improving runs of 4520481612553704665746597month

The first example, with two of its longest improving runs

Text description of the diagram

The chart plots the eight scores of the first example against their months, 0 to 7: 52, 48, 61, 55, 70, 66, 74 and 59.

A solid line joins the scores of months 0, 2, 4 and 6: 52, 61, 70 and 74, which rise every time. A dashed line joins the scores of months 1, 3, 5 and 6: 48, 55, 66 and 74, which also rise every time. Both runs take four months and share the last one, month 6 with 74. The score of month 7, 59, belongs to neither run and is drawn as a plain grey point. No run of five months rises all the way, so 4 is the answer, and the chart shows that a longest run need not be unique.

The brute force, and why it fails at these sizes

Every month is either taken or skipped, and a month can be taken only if its score beats the last score taken. A recursion that tries both choices at every month finds the answer, and the run below counts its calls. When the scores rise every month, every choice is valid, so the calls double with each extra month: 2,047 for ten months and 524,287 for eighteen. Sixty months would take more than 2 × 10¹⁸ calls, and at the limit of 200,000 months the number of calls has 60,207 digits.

Take or skip every month, with the calls counted Python · brute/brute_force.py
"""Improving months by brute force: take or skip every month, and count the calls."""
import math


def longest_by_search(scores):
    """The most months whose scores rise strictly, trying every way to take or skip each month."""
    calls = 0

    def best(i, last):
        # best(i, last): the most months that can still be taken from month i on, above the score `last`.
        nonlocal calls
        calls += 1
        if i == len(scores):
            return 0
        skip = best(i + 1, last)
        if scores[i] > last:
            return max(skip, 1 + best(i + 1, scores[i]))
        return skip

    return best(0, -1), calls  # scores are at least 0, so -1 lies below every score


def months(k):
    return f"{k} month" if k == 1 else f"{k} months"


for scores in ([52, 48, 61, 55, 70, 66, 74, 59], [40, 40, 40], [90, 80, 70]):
    length, calls = longest_by_search(scores)
    print(f"{scores}: {months(length)}, {calls:,} calls")

print()
print("Scores that rise every month are the worst case: every choice of months is a valid one.")
for n in (10, 14, 18):
    length, calls = longest_by_search(list(range(n)))
    print(f"n = {n:2}: {months(length):>9}, {calls:>9,} calls")

# The calls double with every month: 2 ** (n + 1) - 1 for n rising scores.
print(f"n = 60 would make {2 ** 61 - 1:,} calls")
digits = math.floor((200_000 + 1) * math.log10(2)) + 1
print(f"n = 200,000 would make a number of calls with {digits:,} digits")

Output

[52, 48, 61, 55, 70, 66, 74, 59]: 4 months, 180 calls
[40, 40, 40]: 1 month, 10 calls
[90, 80, 70]: 1 month, 10 calls

Scores that rise every month are the worst case: every choice of months is a valid one.
n = 10: 10 months,     2,047 calls
n = 14: 14 months,    32,767 calls
n = 18: 18 months,   524,287 calls
n = 60 would make 2,305,843,009,213,693,951 calls
n = 200,000 would make a number of calls with 60,207 digits

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py

What to notice

  • A run needs only its last score to grow. How it got there does not matter, so the best run that ends at a month can be built from the best runs that end at earlier, lower months.
  • Among runs of the same length, the one with the lowest last score is the most useful, because every later score that extends any of them also extends that one.

Two methods follow from these points. The quadratic DP computes, for every month, the longest improving run that ends there, by looking back at every earlier lower score. At 200,000 months that is about 2 × 10¹⁰ comparisons, minutes of work even in C++, which manages roughly 10⁸ simple steps a second (the constraints lesson measures it). The tails method keeps one number per length, the smallest last score of an improving run of that length, and places each new score by binary search.

The quadratic DP: the best run that ends at each month Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • The tails method: the smallest last score of each length
    • Why the list stays sorted and ends with the right length, proved with an invariant
    • A dry run on the scores
  • Rebuilding one longest run
  • Strictly rising or never falling: bisect_left or bisect_right
  • The code in five languages
  • Measured: comparisons, and how long a random run is
  • Edge cases and common bugs
  • Variants: falling runs, fewest changes, piles and rise-then-fall
  • Exercise: one longest run in O(n log n)
    • One longest improving run in O(n log n)
  • A practice set with sample answers
  • Check yourself
  • Interview questions
    • What is the difference between a longest increasing subsequence and a longest increasing subarray, and how fast can each be found?
    • A candidate's O(n log n) solution prints its final tails list as the longest increasing subsequence, and it passes the examples. Give an input where the output is wrong, explain what the list really holds, and fix the code so that it returns a valid subsequence.
    • Scores must rise strictly from month to month, and you may overwrite as few of them as possible with any whole numbers. A colleague answers n minus the length of the longest increasing subsequence. When is that wrong, and what do you compute instead?
    • Scores arrive one month at a time. After each month an app shows the length of the longest improving run so far and the length of the best improving run that ends this month. What do you keep, what does each month cost, and how would you let the user open the run itself?
  • Key takeaways

9 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 27 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.