Senior 4 min · March 05, 2026

Longest Increasing Subsequence — O(n²) Batch Timeout

O(n²) LIS on 250k prices caused 3-hour risk system timeout.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • LIS finds the longest strictly increasing subsequence (not contiguous) in an array
  • O(n²) DP: dp[i] = 1 + max(dp[j]) for all j
  • O(n log n) patience sorting: maintain tails array, binary search to replace or append
  • Tails array gives correct length but not the actual subsequence
  • Use bisect_left for strict increase; bisect_right for non-decreasing
  • Production trap: O(n²) times out for n>10⁵; O(n log n) handles millions
Plain-English First

Imagine you're collecting stamps and you want the longest run where each new stamp has a higher number than the last — but you can skip stamps that break the chain. You don't have to take every stamp; you just want the longest streak you CAN build by picking and choosing. That's the Longest Increasing Subsequence: find the biggest set of items from a list, in order, where each one is strictly bigger than the one before it.

The Longest Increasing Subsequence (LIS) problem shows up everywhere you need to measure 'how ordered can this data get?' — from DNA sequence alignment in bioinformatics, to version-conflict resolution in distributed systems, to patience sorting algorithms used in real card games. It's deceptively simple to describe but hides serious algorithmic depth. Companies like Google, Meta, and Jane Street use it as a litmus test for whether a candidate truly understands dynamic programming versus just memorising patterns.

The core challenge: given an unsorted array of numbers, find the length (and elements) of the longest subsequence where every element is strictly greater than the previous one. The tricky part is 'subsequence' — you don't need contiguous elements. You can skip as many elements as you like, as long as the ones you pick stay in their original left-to-right order and keep going up. A brute-force check of all 2^n subsets is obviously off the table for anything real-world sized.

By the end of this article you'll understand not one but two fundamentally different approaches — the classic O(n²) DP table and the elegant O(n log n) patience-sort algorithm — know how to reconstruct the actual subsequence (not just its length), handle tricky edge cases like duplicates and negative numbers, and walk into an interview ready to explain the why behind every decision, not just recite the code.

What is Longest Increasing Subsequence (LIS)? — Plain English

The Longest Increasing Subsequence (LIS) problem: given an array of numbers, find the length of the longest subsequence where each element is strictly greater than the previous. Elements do not need to be contiguous.

Example: in [3, 1, 8, 2, 5], the LIS is [1, 2, 5] with length 3 (or [3, 8] is length 2, [1, 5] is length 2).

Real-world uses: scheduling problems where tasks must follow a precedence order, finding the maximum number of non-overlapping intervals that can be stacked, and as a building block for the patience sorting algorithm.

Production Insight
Production systems often need LIS to compare version histories. An O(n²) implementation will crash when you merge 200k-history files.
Patience sorting handles it in milliseconds, so always start with O(n log n) unless you guarantee small input.
Key Takeaway
LIS is about order, not contiguity.
Skipping elements makes it a subsequence, not a subarray.

How Longest Increasing Subsequence (LIS) Works — Step by Step

O(n^2) DP approach: 1. Define dp[i] = length of the LIS ending at index i. 2. Initialize all dp[i] = 1 (each element is an LIS of length 1 by itself). 3. For each i from 0 to n-1: For each j from 0 to i-1: If arr[j] < arr[i]: dp[i] = max(dp[i], dp[j] + 1) 4. Return max(dp).

O(n log n) Binary Search approach (patience sorting): 1. Maintain a 'tails' array where tails[i] = the smallest tail element of all increasing subsequences of length i+1. 2. For each number x in the array: - Binary search for the leftmost position in tails where tails[pos] >= x. - If no such position: append x to tails (extends the longest subsequence). - Else: replace tails[pos] with x (allows a better (smaller tail) subsequence of that length). 3. Return len(tails).

Production Insight
The O(n²) DP uses nested loops — it's fine for n=1000 but becomes a performance bomb at n=100k.
Patience sorting is linear in practice because each element is processed once plus a binary search (O(log n)).
Always profile both on representative data before committing.
Key Takeaway
O(n²) DP: easy to understand, but fails at scale.
O(n log n): harder to grasp, but production-ready.

Worked Example — Tracing the Algorithm

Array: [3, 1, 8, 2, 5, 4, 6]. Find LIS.

O(n^2) DP trace: dp = [1, 1, 1, 1, 1, 1, 1] (start) i=0 (3): no j before. dp[0]=1. i=1 (1): j=0: arr[0]=3 > 1. No update. dp[1]=1. i=2 (8): j=0: 3<8, dp[2]=max(1,dp[0]+1)=2. j=1: 1<8, dp[2]=max(2,dp[1]+1)=2. dp[2]=2. i=3 (2): j=0: 3>2. j=1: 1<2, dp[3]=max(1,dp[1]+1)=2. j=2: 8>2. dp[3]=2. i=4 (5): j=0: 3<5, dp[4]=max(1,2)=2. j=1: 1<5, dp[4]=max(2,2)=2. j=2: 8>5. j=3: 2<5, dp[4]=max(2,3)=3. dp[4]=3. i=5 (4): j=3: 2<4, dp[5]=max(1,3)=3. dp[5]=3. i=6 (6): j=0: 3<6->2. j=3: 2<6->3. j=4: 5<6->4. j=5: 4<6->4. dp[6]=4. dp = [1, 1, 2, 2, 3, 3, 4]. max=4.

O(n log n) patience sort trace: x=3: tails=[3] x=1: 1 replaces 3. tails=[1] x=8: 8 > all. tails=[1,8] x=2: 2 replaces 8. tails=[1,2] x=5: 5 > all. tails=[1,2,5] x=4: 4 replaces 5. tails=[1,2,4] x=6: 6 > all. tails=[1,2,4,6] len(tails)=4. LIS length = 4 (e.g., [1,2,5,6] or [1,2,4,6]).

Production Insight
The O(n²) trace reveals that most comparisons are wasted — you recompute dp for every j. The patience trace shows each element is processed exactly once with a log lookup. In production, that difference means seconds vs hours.
Don't trace by hand for large n — add logging at key decision points instead.
Key Takeaway
Patience sorting is O(n log n) because each element triggers exactly one binary search.
The tails array always gives the correct length, but not the actual sequence.

Implementation

The following implementation demonstrates Longest Increasing Subsequence (LIS) with clear comments.

lis.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import bisect

def lis_dp(arr):
    """O(n^2) DP approach"""
    n  = len(arr)
    dp = [1] * n
    for i in range(1, n):
        for j in range(i):
            if arr[j] < arr[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp) if dp else 0

def lis_binary(arr):
    """O(n log n) patience-sort approach"""
    tails = []
    for x in arr:
        pos = bisect.bisect_left(tails, x)
        if pos == len(tails):
            tails.append(x)
        else:
            tails[pos] = x
    return len(tails)

def lis_reconstruct(arr):
    """O(n log n) with reconstruction"""
    tails = []
    indices = []  # index of tails[i] in original arr
    prev = [-1] * len(arr)
    for i, x in enumerate(arr):
        pos = bisect.bisect_left(tails, x)
        if pos == len(tails):
            tails.append(x)
            indices.append(i)
        else:
            tails[pos] = x
            indices[pos] = i
        # Link predecessor
        if pos > 0:
            prev[i] = indices[pos - 1]
        else:
            prev[i] = -1
    # Reconstruct by tracing back from last element of tails
    k = indices[-1]
    seq = []
    while k != -1:
        seq.append(arr[k])
        k = prev[k]
    seq.reverse()
    return len(seq), seq

arr = [3, 1, 8, 2, 5, 4, 6]
print('O(n^2) LIS:', lis_dp(arr))     # 4
print('O(n log n) LIS:', lis_binary(arr))  # 4
length, seq = lis_reconstruct(arr)
print('LIS length:', length, 'Sequence:', seq)  # 4 [1,2,4,6] or [1,2,5,6]
Output
O(n^2) LIS: 4
O(n log n) LIS: 4
LIS length: 4 Sequence: [1, 2, 4, 6]
Mental Model: The Card Game
  • Each pile is a subsequence ending with that value.
  • You always place a card on the leftmost pile whose top card is >= your card.
  • If no pile qualifies, start a new pile to the right.
  • The number of piles at the end equals the LIS length.
  • The piles themselves are not the LIS — they enable the length calculation.
Production Insight
Always include a reconstruction version in your codebase for debugging. The length alone tells you nothing about which elements form the LIS.
In production, you might need the actual subsequence for logging or audit trails.
Key Takeaway
Reconstruction requires a predecessor array and the indices of tails.
The sequence is built by backtracking from the last tail index.

Edge Cases and Duplicate Handling

Strictly increasing vs non-decreasing: the bisect variant matters. - bisect_left: replaces first element >= x → ensures strict increase (duplicates not allowed to extend). - bisect_right: replaces first element > x → allows equal values to extend the subsequence (non-decreasing).

Negative numbers: the algorithm works with any comparable elements. No special casing needed.

Empty array: return 0.

Single element: return 1.

All decreasing: LIS length = 1.

All equal: LIS length = 1 (strict) or n (non-decreasing).

Large integers: Python's int is arbitrary precision — no overflow. In Java or C++, use long or BigInteger if applicable.

Production Insight
Duplicates cause subtle bugs: using the wrong bisect variant can silently produce incorrect lengths.
Always unit-test with [1,1,1] and [1,2,2,3] to validate strict vs non-decreasing.
Key Takeaway
bisect_left for strict increase; bisect_right for non-decreasing.
Test duhplicates explicitly — they catch most implementation errors.
Choose the Right Algorithm for Your Input
IfInput size n <= 10,000 and no performance constraints
UseUse O(n²) DP for simplicity and readability.
IfInput size n > 10,000 or production latency requirement < 100ms
UseUse O(n log n) patience sorting.
IfNon-decreasing (allow equal values) needed
UseUse bisect_right in patience sorting; in DP, change arr[j] < arr[i] to arr[j] <= arr[i].
IfNeed the actual subsequence, not just length
UseExtend patience sorting with predecessor array and indices tracking.

Why O(n log n) LIS Is Called Patience Sorting

The name comes from the card game of patience (solitaire). Imagine you have a deck of cards in random order. You create piles by placing each card on the leftmost pile whose top card is >= the current card. If no pile qualifies, start a new pile on the right. The number of piles after processing all cards is the LIS length.

This isn't just an analogy — the algorithm was discovered by D.E. Knuth and friends in the 1960s while studying patience sorting, and it directly translates to the tails array.

Why it works: The piles maintain the invariant that the top cards are sorted in increasing order (left to right). Each new card either extends the chain (creates a new pile) or improves an existing pile by lowering its top card. The length of the longest chain is always the number of piles.

Production Insight
The patience sorting algorithm is inherently parallelizable: each card can be processed independently if you batch, but the binary search requires a global view. In distributed systems, you'd need to partition the array and merge partial pile structures — a known area of research.
Key Takeaway
Patience sorting maps naturally to the card game.
The number of piles is the LIS length — always.
● Production incidentPOST-MORTEMseverity: high

O(n²) LIS Causes 3-Hour Batch Timeout in Financial Risk System

Symptom
Risk calculation pipeline timed out after 3 hours on a dataset of 250,000 daily security prices. The team expected the DP to finish in minutes based on their 10,000-item test set.
Assumption
Everyone assumed the O(n²) DP would be fast enough because 'it's just nested loops over an array.' The gap between test data (10³) and production data (2.5×10⁵) wasn't accounted for.
Root cause
O(n²) complexity: n=250,000 means 62.5 billion comparisons. Even with optimized Java, each comparison takes ~10 ns → 625 seconds. Add JIT warmup, garbage collection, and the actual loop overhead → easily exceeds 3 hours.
Fix
Replaced the O(n²) DP with the O(n log n) patience-sort algorithm. The same batch now runs in under 2 seconds. Changed the parent system to enforce use of O(n log n) for any input over 100,000 elements.
Key lesson
  • Always benchmark LIS with production-sized data before deploying.
  • O(n²) is acceptable only for n ≤ 10,000; beyond that, switch to patience sorting.
  • Include a complexity guard: if input size > threshold, enforce O(n log n).
  • Set a timeout alarm for any algorithm with quadratic or higher complexity.
Production debug guideSymptom → Action for common LIS issues5 entries
Symptom · 01
Algorithm returns wrong LIS length for duplicates
Fix
Check if bisect_left or bisect_right is used. For strictly increasing, use bisect_left. For non-decreasing, use bisect_right.
Symptom · 02
Tails array contains elements not in the original array
Fix
That's expected — tails stores smallest possible tails, not the actual subsequence. To reconstruct, maintain a predecessor array.
Symptom · 03
O(n²) DP gives correct but slow response (n > 10,000)
Fix
Profile with a timer. If quadratic growth is confirmed, migrate to patience-sort. Verify input size and add a fallback.
Symptom · 04
Reconstructed LIS length differs from tails length
Fix
The tails length is always the correct LIS length. If reconstructed length is shorter, the predecessor tracking is incorrect — ensure pred only updates when dp[i] actually extends.
Symptom · 05
Memory blow-up with O(n²) DP on large arrays
Fix
dp array is O(n) memory (fine). If you're storing all subsequences, you're doing it wrong — LIS only needs length. Use patience-sort for memory efficiency.
★ Quick LIS Debug Commands (Interview & Production)When your LIS algorithm behaves unexpectedly, run these checks in order.
Wrong length returned
Immediate action
Run on small known array [1,2,3] and [3,2,1]
Commands
assert lis_length([1,2,3]) == 3
assert lis_length([3,2,1]) == 1
Fix now
Check comparison operator: must be < for strict increase.
Reconstruction produces incorrect sequence+
Immediate action
Ensure predecessor array updates only when dp[i] = dp[j] + 1 with arr[j] < arr[i]
Commands
Print pred array alongside dp for a small test
Trace backward from the max-dp index
Fix now
Store pred[i] = j only on a strict improvement (dp[i] < dp[j] + 1).
O(n log n) returns length that seems too large+
Immediate action
Verify duplicate handling: use bisect_left for strict, bisect_right for non-decreasing
Commands
Test array with duplicates: [1,1,1] expects 1 for strict
Test [1,2,2,3] expects 3 for strict, 4 for non-decreasing
Fix now
Change bisect variant and re-run tests.
LIS Algorithm Comparison
PropertyO(n²) DPO(n log n) Patience
Time ComplexityO(n²)O(n log n)
Space ComplexityO(n) for dp + optional predO(n) for tails + optional indices
Easy to implementYesModerate (binary search)
ReconstructionStraightforward with pred arrayNeeds extra indices and pred array
Handles duplicates (strict)Use < comparisonUse bisect_left
Handles non-decreasingUse <= comparisonUse bisect_right
Production readiness (n large)Fails for n > 10,000Handles millions
Interview frequencyVery common (intermediate)Common (senior/advanced)

Key takeaways

1
LIS finds the length of the longest strictly increasing subsequence in O(n²) DP or O(n log n) binary search.
2
O(n²)
dp[i] = length of LIS ending at index i. Check all j<i where arr[j]<arr[i].
3
O(n log n)
maintain a 'tails' array and binary search to replace/append each new element.
4
The tails array gives the correct LIS length but not necessarily the actual subsequence.
5
Use bisect_left for strict increase, bisect_right for non-decreasing.
6
Always test edge cases
empty, single, all equal, all decreasing.
7
Patience sorting is production-ready for millions of elements.

Common mistakes to avoid

5 patterns
×

Using bisect_right for strict increasing LIS

Symptom
LIS length is too high because equal values are allowed to extend the subsequence.
Fix
Use bisect_left for strict increasing LIS. bisect_right gives non-decreasing LIS.
×

Assuming tails array contains the actual LIS

Symptom
When debugging, you print tails and expect it to be a valid increasing subsequence from the input, but it contains numbers not in the original array or out of order relative to the input.
Fix
Remember: tails stores the smallest possible tail for each length, not the actual sequence. To get the real LIS, implement reconstruction with a predecessor array.
×

Not handling empty or single-element arrays

Symptom
IndexOutOfBoundsException or wrong result for empty arrays.
Fix
Guard: if len(arr) == 0: return 0. For DP, handle n=0 before initializing dp.
×

Using O(n²) DP on large inputs without checking complexity

Symptom
Program hangs or times out for inputs > 50,000 elements.
Fix
Set a complexity guard: if n > 10000, fallback to O(n log n). Or always use O(n log n) as default.
×

Confusing subsequence with subarray in recurrence

Symptom
DP logic uses contiguous condition (i-1), not all previous indices.
Fix
The DP recurrence must check all j < i, not just i-1. LIS is non-contiguous.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the recurrence relation for LIS?
Q02SENIOR
How does the O(n log n) LIS algorithm work? Explain with the card game a...
Q03SENIOR
How would you reconstruct the actual LIS, not just its length?
Q04SENIOR
How would you modify LIS to find the longest non-decreasing subsequence?
Q05SENIOR
Can you solve LIS in O(n log n) without using binary search?
Q01 of 05JUNIOR

What is the recurrence relation for LIS?

ANSWER
dp[i] = 1 + max(dp[j]) for all j < i where arr[j] < arr[i]; base case dp[i] = 1. The answer is max(dp).
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Does the tails array in O(n log n) LIS represent an actual LIS?
02
How do you reconstruct the actual LIS, not just its length?
03
What is the difference between strictly increasing and non-decreasing LIS?
04
Can LIS be solved in O(n log n) with integer arrays only?
05
How does the LIS problem relate to patience sorting?
06
Is there a greedy solution for LIS?
🔥

That's Dynamic Programming. Mark it forged?

4 min read · try the examples if you haven't

Previous
Longest Common Subsequence
5 / 15 · Dynamic Programming
Next
Coin Change Problem