Coding Interview Patterns Module 25 – Dynamic programming: knapsack, grids and strings
Interleavings and distinct subsequences
Decide whether a string interleaves two others, and count the ways a short code occurs in a long log: the brute force, its cost and the two-index signal.
What you will learn
- Check whether a string interleaves two others
- Count the distinct ways a pattern occurs as a subsequence
- Handle large counts with a modulus or big integers
- Explain why a greedy merge fails and prove the interleaving table correct
- Optimise both tables to one row, and fill the counting row in the right direction
Before you start
What you’ll learn · Before you start
What you will learn
- Check whether a string interleaves two others
- Count the distinct ways a pattern occurs as a subsequence
- Handle large counts with a modulus or big integers
- Explain why a greedy merge fails and prove the interleaving table correct
- Optimise both tables to one row, and fill the counting row in the right direction
Before you start
On this page
Two classic string tables share one idea: a pair of positions. The interleaving string problem asks whether a string c is a merge of two strings a and b that keeps the order of each; the interleavings of two queues of announcements into one broadcast are exactly such merges. The distinct subsequences problem asks how many different sets of positions in a long string spell a short one, such as an error code in a log. Both brute forces branch at every letter, and both become tables of (n + 1)(m + 1) cells: true or false for interleavings, a count for distinct subsequences.
Two problems: a merged broadcast and a code in a log
Problem A: an interleaving string. A station keeps two queues of announcements, a and b, written as strings of
codes, and one broadcast c. Return whether c is an interleaving of a and b: every letter of both queues
appears in c exactly once, and each queue’s letters keep their order. Limits: each queue has 0 to 1,000 letters.
a = "PQR",b = "XY",c = "PXQYR": true.c = "PYXQR": false, Y comes before X.c = "PXQY": false, R is missing.
Problem B: distinct subsequences. Count the ways the code t occurs in the log s as a subsequence, as different
sets of positions, modulo 1,000,000,007. Limits: up to 10,000 log letters and 100 code letters.
"ERR"in"ERRORERROR": 18 ways."OK"in"OKERRKOERR": 2 ways.
An interleaving keeps each queue in order; a count adds up the ways to pick a code
Text description of the diagram
The upper panel has three rows. The top row is queue a, the letters P, Q and R; the bottom row is queue b, the letters X and Y; the middle row is the broadcast c, the letters P, X, Q, Y and R. Lines join each letter of a queue to its place in the broadcast: P to the first place, X to the second, Q to the third, Y to the fourth and R to the fifth. The lines from a never cross each other, and neither do the lines from b, because each queue keeps its own order inside c.
The lower panel shows the log OKERRKOERR as ten letter boxes. The O in the first place and the K letters in the second and sixth places are highlighted, and the two K letters are labelled 1 and 2. The code OK occurs in the log in exactly two ways: the first O with the K in second place, or the first O with the K in sixth place. No later O has a K after it.
The brute force, and why it fails at these sizes
For Problem A, take the next letter of c from a if it fits and solve the rest; if that fails, take it from b.
When the queues share letters both branches can live for a long time: with 20 letters that are all equal except the
last, the search makes 520,675 calls before it answers false, about 15 times more for every 4 more letters. For
Problem B, try every set of positions: a code of 3 letters in a log of 10,000 letters gives 166,616,670,000 sets.
# Both problems by brute force. Interleaving: at each letter of the broadcast, try taking it from the first queue and,
# failing that, from the second. Counting: try every set of positions in the log. The work is counted.
from itertools import combinations
from math import comb
def interleaves(a, b, c, i=0, j=0, calls=None):
"""Is c[i + j:] an interleaving of a[i:] and b[j:]?"""
calls[0] += 1
if i + j == len(c):
return i == len(a) and j == len(b)
take_a = i < len(a) and a[i] == c[i + j] and interleaves(a, b, c, i + 1, j, calls)
return take_a or (j < len(b) and b[j] == c[i + j] and interleaves(a, b, c, i, j + 1, calls))
def count_ways(log, code):
"""(ways, position sets tried): every choice of len(code) positions of the log is read and compared."""
tried = ways = 0
for picks in combinations(range(len(log)), len(code)):
tried += 1
ways += all(log[p] == ch for p, ch in zip(picks, code))
return ways, tried
for a, b, c in [("PQR", "XY", "PXQYR"), ("PQR", "XY", "PYXQR")]:
calls = [0]
print(f"{c!r} from {a!r} and {b!r}: {interleaves(a, b, c, calls=calls)}, {calls[0]} calls")
for n in (6, 8, 10):
a = b = "A" * n
c = "A" * (2 * n - 1) + "B" # all letters equal except the last: every split is tried before the answer, False
calls = [0]
print(f"{2 * n} letters, every one but the last equal: {interleaves(a, b, c, calls=calls)}, {calls[0]:,} calls")
for log, code in [("ERRORERROR", "ERR"), ("OKERRKOERR", "OK")]:
print(f"{code!r} in {log!r}: {count_ways(log, code)}")
print(f"a code of 3 letters in a log of 10,000: {comb(10_000, 3):,} position sets") Output
'PXQYR' from 'PQR' and 'XY': True, 6 calls 'PYXQR' from 'PQR' and 'XY': False, 2 calls 12 letters, every one but the last equal: False, 2,507 calls 16 letters, every one but the last equal: False, 35,749 calls 20 letters, every one but the last equal: False, 520,675 calls 'ERR' in 'ERRORERROR': (18, 120) 'OK' in 'OKERRKOERR': (2, 45) a code of 3 letters in a log of 10,000: 166,616,670,000 position sets
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
What to notice
- The broadcast position is not a third index. After i letters of
aand j ofb, the next letter ofcis at position i + j, so the pair (i, j) says everything: about a million pairs at the limits. - A count makes two choices per log letter: skip it, or use it for the next code letter when they are equal. The state is how much of the log has been read and how much of the code is spelled.
The interleaving table marks the pairs (i, j) that a valid merge can reach, and one row of it is enough. Taking a letter from the first queue whenever it fits is a greedy shortcut that fails. The counting table adds up the two choices, and one row filled from right to left uses each log letter once. Counts past 64 bits need a modulus or big integers.
The interleaving table Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the table is right
- A dry run, and the greedy that fails
- Interleaving in one row of memory
- Counting distinct subsequences: the table
- One row, from right to left
- Counts that outgrow 64 bits
- The code in five languages
- Edge cases and common bugs
- Variants on the same tables
- Exercise: the ways a code occurs in a log
- The ways a code occurs in a log
- A practice set with sample answers
- Check yourself
- Interview questions
- Why must an interleaving check compare len(a) + len(b) with len(c) before it fills its table?
- A colleague checks interleavings by taking each letter of c from a whenever a's next letter fits, and from b otherwise. It passes every sample test. Is it correct? Give the smallest counterexample, the fix and what the fix costs.
- An audit needs the exact number of ways a 40-character code occurs as a subsequence of a log of 100,000 characters, not a remainder. What do you store, what does it cost in time and memory, and how do you test it?
- Three services each emit an ordered stream of events, and a collector writes one merged log. How do you check that the log is a valid interleaving of the three streams, and how does the cost grow with the number of streams?
- Key takeaways
1 diagram · 10 runnable examples (C++, Go, Java, JavaScript, Overflow.java, Python) · 1 exercise with sample tests · 3 interview answers · about 22 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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
- Integers have unlimited precision: numeric types int, float, complex (The Python Standard Library) (Python Software Foundation)
- math.comb (The Python Standard Library) (Python Software Foundation)
- itertools.combinations (The Python Standard Library) (Python Software Foundation)
- ECMAScript 2026 Language Specification: Number.MAX_SAFE_INTEGER (Ecma International)
- ECMAScript 2026 Language Specification: the BigInt type (Ecma International)
- The Java Language Specification, Java SE 25: 15.18.2 Additive operators (+ and -) for numeric types (Oracle)
- java.math.BigInteger (Java SE 25 API) (Oracle)
- The Go Programming Language Specification: integer overflow (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress