Senior 8 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 & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is Longest Increasing Subsequence?

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.

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.

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.

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.
LIS O(n²) Algorithm Flow THECODEFORGE.IO LIS O(n²) Algorithm Flow From brute force to memoized DP for longest increasing subsequence Brute Force Recursion Explore all subsequences; O(2^n) time Memoization Table Store LIS length for each index DP Transition dp[i] = max(dp[j]+1) for j Duplicate Handling Strictly increasing: skip equal values O(n²) DP Result max(dp) gives LIS length ⚠ Forgetting strict increase on duplicates Only allow nums[j] < nums[i], not ≤ THECODEFORGE.IO
thecodeforge.io
LIS O(n²) Algorithm Flow
Longest Increasing Subsequence

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.

The Recursive Decomp — Why Brute Force Gets You Fired

Before we get clever with binary search, understand the cost of naivety. The recursive approach reads like a textbook: for each element at index i, look left for every smaller element, recursively compute their LIS, take max, add 1. Problem? You're recomputing the same subproblems across overlapping branches—exponential complexity in real terms.

For an array of 40 elements, you're looking at over a trillion recursive calls. That's not an edge case, that's a production outage waiting to happen. Every recursive call spawns a new stack frame, and for any real dataset (think: financial time series, log sequences), this burns through memory and time simultaneously.

The formula looks clean: L(i) = 1 + max(L(prev)) for all prev < i with arr[prev] < arr[i]. But clean on paper means nothing when your monitoring dashboard is red. Recursion is a learning tool, not a deployment strategy.

LISRecursive.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — dsa tutorial

public class LISRecursive {
    public static int lengthOfLIS(int[] sequence) {
        return solve(sequence, Integer.MIN_VALUE, 0);
    }

    private static int solve(int[] seq, int lastValue, int currentIndex) {
        if (currentIndex == seq.length) {
            return 0;
        }
        int exclude = solve(seq, lastValue, currentIndex + 1);
        int include = 0;
        if (seq[currentIndex] > lastValue) {
            include = 1 + solve(seq, seq[currentIndex], currentIndex + 1);
        }
        return Math.max(include, exclude);
    }

    public static void main(String[] args) {
        int[] data = {3, 10, 2, 1, 20};
        System.out.println("LIS length: " + lengthOfLIS(data));
    }
}
Output
LIS length: 3
Production Trap: Recursion Depth
Java's default stack size (~1MB) kills recursion at ~10,000 elements. If your LIS candidate input is a server log stream, this blows up before you even compute the first subsequence.
Key Takeaway
Recursion on LIS is O(2^n). For anything larger than n=30, your runtime becomes a business problem.

Memoization — Buying Time by Storing What You Already Paid For

You identified the waste. Now fix it. Memoization caches results for each (index, lastValue) pair—but here's the twist: lastValue is unbounded, so you index by the previous element's position instead. This gives O(n^2) time and O(n^2) space. Still quadratic, but the difference between 2^40 and 40^2 is the difference between a coffee break and a termination meeting.

The trick: dp[i][prev] or, more practically, a 1D array where memo[i] stores the LIS length ending at index i. Fill it top-down with recursion but consult the cache before branching. This transforms your exponential mess into something that actually finishes before your CI pipeline times out.

Memoization shines when only a subset of states are reachable—but for LIS? Every element is a potential start or end. You're still paying O(n^2) in the worst case. It's better, but not production-grade for large sequences. Use this as a stepping stone to the bottom-up DP, not as your final answer.

LISMemoization.javaJAVA
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
// io.thecodeforge — dsa tutorial

import java.util.Arrays;

public class LISMemoization {
    public static int lengthOfLIS(int[] sequence) {
        int n = sequence.length;
        int[] memo = new int[n];
        Arrays.fill(memo, -1);
        int maxLen = 0;
        for (int i = 0; i < n; i++) {
            maxLen = Math.max(maxLen, solve(sequence, i, memo));
        }
        return maxLen;
    }

    private static int solve(int[] seq, int current, int[] memo) {
        if (memo[current] != -1) return memo[current];
        int best = 1;
        for (int prev = 0; prev < current; prev++) {
            if (seq[prev] < seq[current]) {
                best = Math.max(best, 1 + solve(seq, prev, memo));
            }
        }
        memo[current] = best;
        return best;
    }

    public static void main(String[] args) {
        int[] data = {3, 10, 2, 1, 20};
        System.out.println("LIS length: " + lengthOfLIS(data));
    }
}
Output
LIS length: 3
Senior Shortcut: Cache Key Design
Using array index as the state key is clean. Don't overcomplicate with HashMap unless you have sparse dependencies. Keep it linear and readable.
Key Takeaway
Memoization flips exponential to O(n^2) and is your first real optimization. Still quadratic, but your code might survive a code review.

Bottom-Up DP — Tabulate or Die Trying

Recursion with memoization still carries recursion overhead. For a function called 40 million times, those stack frames add up. Bottom-up DP eliminates that entirely: iterate left to right, maintain a dp[i] array where each entry is the LIS length ending at index i. For each i, scan all j < i, check if arr[j] < arr[i], and set dp[i] = max(dp[i], dp[j] + 1).

Code's shorter. Performance is predictable. No recursion limits, no stack overflow in production. Time is still O(n^2), but space collapses to O(n). This is the version you ship to staging for validation before switching to the O(n log n) Patience Sorting version.

Watch your edge cases: all equal elements? Prep dp with 1 for each position. Duplicates? The strict < comparison handles it—if you see arr[j] == arr[i], skip it. This is where junior devs introduce bugs by using <= instead of <. One character, and your LIS magically includes duplicates, inflating your metric. Measure twice, deploy once.

LISBottomUp.javaJAVA
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
// io.thecodeforge — dsa tutorial

public class LISBottomUp {
    public static int lengthOfLIS(int[] sequence) {
        int n = sequence.length;
        if (n == 0) return 0;
        int[] dp = new int[n];
        int maxLen = 1;
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (sequence[j] < sequence[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        return maxLen;
    }

    public static void main(String[] args) {
        int[] data = {3, 10, 2, 1, 20};
        System.out.println("LIS length: " + lengthOfLIS(data));
    }
}
Output
LIS length: 3
Production Trap: Off-by-One in Nested Loop
Double-check your j < i bound. If you accidentally use j <= i or start from j = 1, you'll either skip the first element or overflow the array. This bug is silent—your LIS will just be slightly wrong by 1.
Key Takeaway
Bottom-up DP is O(n^2) time, O(n) space, and recursion-free. It's the reliable middle ground before your final O(n log n) optimization.

Generalize the Relationship — LIS Is Hiding in Plain Sight

You think LIS is just about numbers? Wrong. LIS is a pattern for any sequence where order matters and you're looking for the longest chain that respects a comparator. Once you see that, LIS stops being a puzzle and becomes a weapon.

The core relationship is simple: for every element at index i, the longest increasing subsequence ending at i is 1 plus the maximum LIS ending at any previous index j where arr[j] < arr[i]. That's not just for integers — it works for strings, tuples, or any comparable type. The comparator defines "increasing".

In production, this generalizes to problems like "longest chain of boxes that fit inside each other" or "maximum number of events you can attend without overlap". You sort by one dimension, then run LIS on the other. Same DP. Same O(n log n) time when you optimize. Stop memorizing problems — generalize the relationship and watch the grind melt away.

GeneralizedLIS.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.theforge — dsa tutorial

import java.util.*;

public class GeneralizedLIS {
    // Generic LIS for any Comparable type
    public static <T extends Comparable<T>> int lengthOfLIS(List<T> arr) {
        List<T> tails = new ArrayList<>();
        for (T x : arr) {
            int pos = Collections.binarySearch(tails, x);
            if (pos < 0) pos = -(pos + 1);
            if (pos == tails.size()) tails.add(x);
            else tails.set(pos, x);
        }
        return tails.size();
    }

    public static void main(String[] args) {
        List<Integer> arr = Arrays.asList(3, 1, 8, 2, 5);
        System.out.println(lengthOfLIS(arr));  // Output: 3
    }
}
Output
3
Senior Shortcut:
Binary search on tails gives you O(n log n). The moment you see 'longest increasing subsequence' in any problem, reach for tails — not the O(n²) DP.
Key Takeaway
LIS generalizes to any comparable sequence: define your comparator, then the same O(n log n) tails technique solves it.

Step-by-Step: Trace [3, 1, 8, 2, 5] — Watch the Tails Grow

Stop reading abstract theory. Let's walk the classic O(n log n) tails algorithm on [3, 1, 8, 2, 5] so you see exactly why it works.

Initialize tails list empty. Process each number
  • 3: tails empty, add → tails = [3]
  • 1: binary search finds position 0 (since 1 < 3). Replace tails[0] with 1 → tails = [1]
  • 8: falls after everything, append → tails = [1, 8]
  • 2: binary search finds position 1 (since 2 < 8). Replace tails[1] with 2 → tails = [1, 2]
  • 5: falls after everything, append → tails = [1, 2, 5]

Length of tails at end = 3. That's the LIS length. Notice tails is NOT the actual subsequence — it's the smallest possible tail of an increasing subsequence of each length. When you replace 8 with 2, you're saying "an LIS of length 2 can end at 2 instead of 8", which opens the door for 5 to extend to length 3.

This is not magic. It's greedy optimization: keep the lowest possible ending value for each subsequence length so future elements have the best chance to extend it.

TraceLIS.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — dsa tutorial

import java.util.*;

public class TraceLIS {
    public static void main(String[] args) {
        int[] arr = {3, 1, 8, 2, 5};
        List<Integer> tails = new ArrayList<>();
        
        for (int x : arr) {
            int pos = Collections.binarySearch(tails, x);
            if (pos < 0) pos = -(pos + 1);
            if (pos == tails.size()) tails.add(x);
            else tails.set(pos, x);
            System.out.println("After " + x + ": tails = " + tails);
        }
        System.out.println("LIS length: " + tails.size());
    }
}
Output
After 3: tails = [3]
After 1: tails = [1]
After 8: tails = [1, 8]
After 2: tails = [1, 2]
After 5: tails = [1, 2, 5]
LIS length: 3
Production Trap:
Don't assume tails contains the actual LIS. It only stores the best possible end values. To reconstruct the sequence, you need parent pointers — don't skip that in real code.
Key Takeaway
The tails array always stays sorted; its final length is the LIS length. Replacing values greedily keeps the door open for longer subsequences.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Dynamic Programming. Mark it forged?

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