Subsequence vs Substring: Why One Word Changes Your Algorithm

- A substring must be contiguous; a subsequence only needs to preserve relative order, gaps allowed
- A string of length n has n(n+1)/2 substrings but 2^n subsequences, which is why brute force breaks
- Substring problems reach for sliding window (O(n)); subsequence problems reach for dynamic programming (O(n·m))
- Check if t is a substring with KMP in O(n+m); check if t is a subsequence with two pointers in O(n), O(1) space
- The reset-vs-no-reset rule is the entire difference between longest common substring DP and longest common subsequence DP
- Always ask "does the sequence need to be contiguous?" before writing a line of code — the answer determines your algorithm
You're staring at a problem. It says "longest common subsequence." Your brain parses this as: sequence, string, same thing basically, I've got a template for this. You reach for the sliding window you've drilled 30 times. Ten minutes in, nothing makes sense. Fifteen minutes in, you're questioning your career choices.
The interviewer is watching you dig the hole deeper.
This happens constantly. The words look almost identical. They are not. Confusing them doesn't just slow you down, it sends you down a completely wrong algorithmic path with no obvious place to catch the mistake because the code you write is technically valid. It just solves a different problem.
Here's the distinction, why it matters, and how interviewers use it to separate candidates who understand strings from those who've memorized patterns.
A Substring Is a Contiguous Slice
A substring is a sequence of characters that appear consecutively and in order within a string. No gaps. No skipping. A contiguous block.
Take the string "abcde". Its substrings include:
"a","b","c","d","e"(length 1)"ab","bc","cd","de"(length 2)"abc","bcd","cde"(length 3)"abcd","bcde"(length 4)"abcde"(length 5)""(the empty string, depending on your definition)
Notice: "ace" is not in that list. You cannot skip b and d to arrive at "ace" as a substring.
A string of length n has exactly n*(n+1)/2 non-empty substrings. For n=5, that's 15. They cover the entire string in contiguous windows, which is exactly why the sliding window algorithm works on substring problems. The window is always one solid chunk.
A Subsequence Preserves Order but Allows Gaps
A subsequence is a sequence derived from another string by deleting zero or more characters, without changing the relative order of the remaining characters.
The gaps don't matter. What matters is that you keep things left-to-right.
From "abcde", valid subsequences include:
"ace"(skipbandd)"bd"(skipa,c,e)"abcde"(skip nothing, every string is a subsequence of itself)"a","e","ad"(any characters in their original left-to-right order)""(the empty string is always a valid subsequence)
Not a valid subsequence: "ec". This reverses the order. e comes after c in the original, so you can't pull them out in that order.
A string of length n has 2^n subsequences, because for each character you independently choose to include it or skip it. For n=5, that's 32. For n=30, that's over a billion. This is the first hint that brute-force won't work and that dynamic programming is coming.
The Visual That Makes It Click

String: a b c d e
Index: 0 1 2 3 4
Substring "bcd": _ [b c d] _ ← contiguous block, no gaps
Subsequence "bd": _ [b] _ [d] _ ← gap over c, order preserved
Every substring is also a subsequence. The reverse is not true. This asymmetry is the whole game.
How You Actually Check Each One
The checking algorithms are completely different, and that difference is the first sign of which problem family you're in.
Checking if t is a substring of s:
The naive approach is O(n*m) where n is the length of s and m is the length of t. Python's in operator is this under the hood.
def is_substring(s: str, t: str) -> bool: return t in s # O(n*m) naive, or O(n+m) with KMP internally
The fast version is the KMP algorithm, which preprocesses the pattern in O(m) and then searches in O(n). Most standard libraries use an optimized substring search, so you rarely implement it by hand. The point is: you're looking for a window in the source string that exactly matches the pattern.
Checking if t is a subsequence of s:
Two pointers. One scans s forward. One scans t forward. Advance the t pointer only when you find a match.
def is_subsequence(s: str, t: str) -> bool: i, j = 0, 0 while i < len(s) and j < len(t): if s[i] == t[j]: j += 1 i += 1 return j == len(t)
This runs in O(n) time, O(1) space. If you reach the end of t (meaning j == len(t)), every character was matched in order. It works because you only need characters to appear in order, not consecutively. The same logic fails for substrings because you can't skip characters in the source when looking for a contiguous match.
Why This Ruins Your Algorithm if You Get It Wrong
When a problem is about substrings, the constraint is contiguity. That constraint is what makes sliding window work: you maintain a window of valid characters and slide it forward. The window never has internal gaps.
When a problem is about subsequences, contiguity is gone. Now you're choosing which characters to include. That's a combinatorial decision. Combinatorial decisions with overlapping subproblems mean dynamic programming, not sliding windows. Your sliding window template won't fail with a compile error. It'll fail silently and produce wrong answers on half the test cases.
Here's the full pattern:
| Problem type | Algorithm family | Typical complexity |
|---|---|---|
| Longest substring without repeating chars | Sliding window | O(n) |
| Longest common substring | DP or sliding window | O(n×m) |
| Longest common subsequence | DP | O(n×m) |
| Longest increasing subsequence | DP or patience sorting | O(n²) or O(n log n) |
Is t a substring of s? | KMP or naive | O(n+m) or O(n×m) |
Is t a subsequence of s? | Two pointers | O(n) |
Reach for the wrong tool and you're solving a different problem at the wrong complexity.
The Classic Interview Problems, Separated
Knowing which category a problem falls into is half the battle. Your pattern recognition needs to fire at the word level, not after ten minutes of failed code.
Substring problems (contiguity is the constraint):
- LeetCode 3: Longest Substring Without Repeating Characters. A sliding window expands right and contracts left when a duplicate enters. The window is always a valid substring.
- LeetCode 5: Longest Palindromic Substring. Expand around every center and find the longest contiguous palindrome.
- LeetCode 76: Minimum Window Substring. Sliding window that must contain all characters of the target.
Subsequence problems (order without contiguity):
- LeetCode 392: Is Subsequence. Two-pointer check in
O(n). - LeetCode 1143: Longest Common Subsequence. Classic
O(n*m)DP wheredp[i][j]represents the LCS of the firsticharacters of one string and firstjof the other. - LeetCode 300: Longest Increasing Subsequence. The characters don't have to be adjacent, just increasing in value from left to right.
The LCS problem is worth pausing on. A common beginner mistake is confusing it with Longest Common Substring. The substring version requires contiguity, so dp[i][j] resets to 0 whenever characters don't match. The subsequence version does not reset. Two similar-looking DP tables with completely different update rules, for problems that look nearly identical on the surface.
# Longest Common SUBSTRING (reset on mismatch) def lcs_substring(s1: str, s2: str) -> int: dp = [[0] * (len(s2) + 1) for _ in range(len(s1) + 1)] result = 0 for i in range(1, len(s1) + 1): for j in range(1, len(s2) + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 result = max(result, dp[i][j]) # contiguity broken, dp[i][j] stays 0 return result # Longest Common SUBSEQUENCE (don't reset, take max of skipping) def lcs_subsequence(s1: str, s2: str) -> int: dp = [[0] * (len(s2) + 1) for _ in range(len(s1) + 1)] for i in range(1, len(s1) + 1): for j in range(1, len(s2) + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip a char from either return dp[len(s1)][len(s2)]
Same outer structure. Different inner logic. The reset vs no-reset is the entire difference. One line is where the whole question lives.
The Interview Trap (and the Magic Phrase That Defuses It)
Interviewers know this confusion exists. Some of them count on it.
A common setup: describe a problem in terms that sound like a substring problem but are actually a subsequence problem, or vice versa. The signal they're looking for is whether you ask the right question before writing a single line of code.
That question is: "Does the sequence need to be contiguous?"
Five words. That's it. Asking this tells your interviewer you understand the distinction at a structural level, not just as a memorized fact. It also determines your entire algorithmic approach before you've touched a keyboard. A sliding window runs in O(n). A DP table might run in O(n*m). Choosing the wrong one on a contiguous problem can mean passing all test cases but failing the complexity analysis. Choosing the wrong one on a non-contiguous problem means your solution is simply incorrect.
There's also a space angle. Substring algorithms often run in O(1) or O(k) space, where k is the alphabet size. Subsequence DP typically uses an O(n*m) table, though you can often reduce it to O(min(n, m)) by observing that you only look back one row at a time.
When you see a problem involving two strings and any kind of "common" or "longest" measure, your first question should be whether the constraint is contiguity or just order. Answer that and the algorithm writes itself.
The SpaceComplexity Edge
These two concepts show up constantly in voice-based mock interviews at SpaceComplexity. The candidates who pass aren't necessarily the ones who know the LCS algorithm cold. They're the ones who catch the terminology before writing a line of code and confirm out loud which variant the problem is asking for. That's a communication skill, not a memorization skill. And it only develops through practice under pressure, not by reading another editorial.