Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Dynamic Programming
MediumLC 91

Decode Ways (LeetCode 91)

Target Frequency:AmazonGoogleMeta

A digit-only string was produced by encoding letters through the mapping "1" -> 'A', "2" -> 'B', ..., "25" -> 'Y', "26" -> 'Z'. Because some two-digit codes contain smaller valid one-digit codes inside them ("2" and "5" versus "25"), the same digit string can often be split into letters in more than one way.

For example, the digits "11106" can be grouped as (1, 1, 10, 6) to spell "AAJF", or as (11, 10, 6) to spell "KJF". Grouping it as (1, 11, 06) is not allowed, because "06" is not a valid code -- only "6" is.

Given a digit string s, count how many distinct ways it can be decoded this way, returning 0 when no valid decoding exists at all. Inputs are limited so the count always fits in a 32-bit integer.

Worked Examples

Example 1
Input:s = "12"
Output:2
Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).
Example 2
Input:s = "226"
Output:3
Explanation: "226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).
Example 3
Input:s = "06"
Output:0
Explanation: The leading zero makes "06" an invalid code -- only "6" maps to "F" -- so this digit string has no valid decoding, giving a count of 0.

⚖️Formal Constraints & Bounds

  • 1 <= s.length <= 100

  • s contains only digits and may contain leading zero(s).

Deep-Dive & Conceptual Insights

Core Insight

The ways to decode s[index:] depend only on index, never on how the digits before it were split. So dfs(index) is computed once, stored in memo[index], and every other path that reaches the same index reuses it.

Real-World Scenario & Production Applications

Variable-length binary protocol decoding, telephony DTMF sequence parsing.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: One Question per Index

dfs(index) answers: how many ways can s[index:] be decoded? The answer depends on index alone, so it can be stored under index.

Mathematical Recurrence / Code Invariant
if index == len(s):
    return 1
if s[index] == "0":
    return 0

Step-by-Step Execution Trace Table

Code / Blueprint
"226" (Root, d=0, Index 0)
/ \
1-digit ('2') / \ 2-digits ('22')
/ \
"26" (d=1, Index 1) "6" (d=1, Index 2)
/ \ |
1-digit ('2') / 2-digits \ 1-digit ('6') | 1-digit ('6')
/ ('26') \ |
"6" (d=2, Index 2) "" (d=2, Index 3) "" (d=2, Index 3) "" (d=2, Index 3)
| ✅ (1 way) ✅ (1 way) ✅ (1 way)
1-digit ('6')|
"" (d=3, Index 3)
✅ (1 way)
memo[2] = 1 way memo[1] = 1+1 = 2 ways memo[0] = 2+1 = 3 ways
 
Step-by-Step Decision Trace
  1. Step 1 (Root, d=0, Index 0, "226"):
    • Branch 1: Single digit '2' ('B') →\rightarrow→ calls dfs(1).
    • Branch 2: Double digit '22' ('V', 10≤22≤2610 \le 22 \le 2610≤22≤26) →\rightarrow→ calls dfs(2).
  2. Step 2 (Branch 1: Single digit '2', d=1, Index 1, "26"):
    • Branch 1: Single digit '2' ('B') →\rightarrow→ calls dfs(2).
    • Branch 2: Double digit '26' ('Z', 10≤26≤2610 \le 26 \le 2610≤26≤26) →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
  3. Step 3 (Branch 1: Single digit '2', d=2, Index 2, "6"):
    • Single digit '6' ('F') →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
    • Cache memo[2] = 1.
  4. Step 4 (Aggregate Index 1, d=1): dfs(1) = dfs(2) + dfs(3) = 1 + 1 = 2 ways. Cache memo[1] = 2.
  5. Step 5 (Branch 2: Double digit '22', d=1, Index 2, "6"):
    • Single digit '6' ('F') →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
  6. Step 6 (Aggregate Index 0, d=0): dfs(0) = dfs(1) + dfs(2) = 2 + 1 = 3 ways. Cache memo[0] = 3. Record Total Valid Decodings: 3 ("BZ", "VF", "BBF") ✅. Total Valid Decodings: 3 ("BZ", "VF", "BBF").
Code Shape
Archetype Code Shape
Python
1memo = {}
2def dfs(index):
3 if index == len(s):
4 return 1
5 if s[index] == '0':
6 return 0
7 if index in memo:
8 return memo[index]
9 res = dfs(index + 1)
10 if index + 1 < len(s) and (s[index] == '1' or (s[index] == '2' and s[index + 1] <= '6')):
11 res += dfs(index + 2)
12 memo[index] = res
13 return res
14return dfs(0)

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

A message containing letters from A-Z is encoded to numbers using '1' -> 'A', ..., '26' -> 'Z'. Given a string of digits, how many ways can it be decoded? At any index i, the decoder faces a two-way branch:

  1. Single-digit branch: Decode s[i] if s[i] != '0'.
  2. Double-digit branch: Decode s[i:i+2] if the two-digit value is between 10 and 26. Digits like '0' have no individual letter mapping and cannot begin a valid two-digit code (e.g. "06" is invalid). Top-down backtracking with memoization counts the total valid leaves in this branching tree in linear O(N)O(N)O(N) time.
🏟️ The Analogy: The Telegraph Operator Reading Morse Numbers

Imagine a telegraph operator receiving a stream of digits on ticker tape: "226". The operator looks at the first digit: "2" can be 'B'. If the operator decodes "2" as 'B', the remaining tape is "26". But "22" could also be 'V'. If the operator decodes "22" as 'V', the remaining tape is "6". At each step, the operator branches into 1-character or 2-character decodings, dropping invalid sequences (like leading zeros) into the wastebasket.

🔑 Why It Works

Notice the base cases: reaching the end of the string (index == len(s)) contributes 111 valid decoding path, while encountering a standalone '0' immediately aborts the branch with 000 ways. Memoizing by index ensures each digit index is processed at most once.

  • A memo key that misses an argument: memo must be keyed by every argument that changes the answer. Here index alone decides the ways to decode s[index:]; in Interleaving String (LC 97) the answer depends on how far both strings are used, so the key is (i, j), and keying by i alone mixes up calls that used different amounts of s2.

  • Reading a stored 0 as missing: if memo.get(index): treats a stored 0 as absent, so every index before an unreadable "30" is explored again on every call. Test if index in memo:.

  • Returning without storing: memo[index] = res must run before return res, or the cache never fills and the run is as slow as the plain recursion (exponential on a long run of "1"s).

  • Leading zero: '0' has no letter of its own and never starts a two-digit code ('06' is invalid), so dfs returns 0 as soon as s[index] == "0".

  • Two-digit range: only 10 through 26 are codes: s[index] == "1", or s[index] == "2" and s[index + 1] <= "6".

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement asks to "count how many distinct ways" the digits "can be decoded", and says "the same digit string can often be split into letters in more than one way": each position has a one-digit and a two-digit choice, so the plain recursion branches twice per digit and reaches the same index along many different splits. The count for the rest of the string, s[index:], does not depend on how the digits before it were split.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Invariant: memo[index] is written only with the finished number of ways to decode s[index:], so every call after the first one for the same index returns it without exploring again. index is the whole key because it is the only argument that changes the answer.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Leading zero and range: return 0 when s[index] == "0", and read two digits only for 10 to 26 ("06" and "27" are not codes).

The 60-Second Interview Pitch

Say this out loud before you type a single line

"I'll use Memoization. The plain recursion is easy: at each index I read one digit, or two digits when they form 10 to 26, and add up the ways from what is left. But it reaches the same index along many different splits, so it is exponential. The number of ways to decode the rest of the string depends only on index, not on how I split the digits before it, so I store dfs(index) in memo the first time it finishes and return it on every later call. The trap is the key: it must hold every argument that changes the answer. Here that is index alone. I also test 'index in memo', because a stored zero is a real answer. Each of the N plus one indices is solved once with constant work, so O(N) time, and the memo plus the recursion stack take O(N) space."

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: dfs explores (gets past if index in memo) at most once per value of index, 0 to N, because the first call that finishes writes memo[index] = res and every later call for that index returns at the lookup. An exploring call does constant work (two digit checks, one store) and makes at most two calls, dfs(index + 1) and dfs(index + 2). Each of those calls either explores (at most N + 1 of them in total) or returns in O(1). So there are at most N + 1 exploring calls and 2(N + 1) quick ones: O(N).

SPACE COMPLEXITY

O(N) memo and recursion stack

Look at the allocations: memo holds at most one entry per index that explores, so at most N entries. The recursion goes one level deeper per digit read, so the call stack is at most N + 1 frames deep. The answer is one integer. Total O(N), memo and recursion stack.

Formal Recurrence Relation

T(N) = (N + 1) exploring calls x O(1) + 2(N + 1) lookups x O(1) = O(N)

Derivation Progression

Distinct arguments

index in 0..N -> N + 1 values

The answer of dfs(index) depends on index alone, so there are only N + 1 different questions.

Explore once

memo[index] = res before return res

The first call for an index explores; every later call for it stops at if index in memo and returns the stored value.

Work per exploring call

O(1) + at most 2 calls

Two digit checks, dfs(index + 1), dfs(index + 2) when the two digits form 10 to 26, and one store.

Total

(N + 1) x O(1) + 2(N + 1) x O(1) = O(N)

At most N + 1 calls explore and each makes at most two calls, so at most 3(N + 1) calls run in all, each doing constant work outside its children.

Variable Definitions

NNN

len(s), the number of digits

Memory Architecture & Bounds

🟣 Call Stack

O(N): one frame per digit read, at most N + 1 deep

🔵 Auxiliary Heap

O(N): memo, at most one entry per index

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(1)O(1)O(1): s[0] == "0" returns 0 at once

Average Case

O(N)O(N)O(N)

Worst Case

O(N)O(N)O(N): a string of 1s and 2s, where every index branches twice

State-Space & Subproblem DAG(Directed Acyclic Graph) Topology

State-Space & Subproblem DAG(Directed Acyclic Graph) Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "count how many distinct ways", "can often be split into letters in more than one way". Each digit opens a one-digit and a two-digit branch, and many split paths reach the same index: Memoization, one stored answer per index.

CONSTRAINTS & BOUNDS

1≤s.length≤1001 \le s.length \le 1001≤s.length≤100. The plain recursion makes about 1.6N1.6^N1.6N calls on a run of 1s (317,810 calls for 25 ones); with the memo at most 3(N+1)3(N + 1)3(N+1) calls run. A recursion depth of 101 frames is far below Python's default limit of 1,000.

FAANG PRODUCTION TRAPS & EDGE CASES

Where the cache lives: create memo (or the functools.cache-wrapped dfs) inside numDecodings, so every call starts empty. A module-level cache keeps every earlier string's answers in memory and grows with every call.

Theory Context•Dynamic Programming
MediumLC 91

Decode Ways (LeetCode 91)

Target Frequency:AmazonGoogleMeta

A digit-only string was produced by encoding letters through the mapping "1" -> 'A', "2" -> 'B', ..., "25" -> 'Y', "26" -> 'Z'. Because some two-digit codes contain smaller valid one-digit codes inside them ("2" and "5" versus "25"), the same digit string can often be split into letters in more than one way.

For example, the digits "11106" can be grouped as (1, 1, 10, 6) to spell "AAJF", or as (11, 10, 6) to spell "KJF". Grouping it as (1, 11, 06) is not allowed, because "06" is not a valid code -- only "6" is.

Given a digit string s, count how many distinct ways it can be decoded this way, returning 0 when no valid decoding exists at all. Inputs are limited so the count always fits in a 32-bit integer.

Worked Examples

Example 1
Input:s = "12"
Output:2
Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).
Example 2
Input:s = "226"
Output:3
Explanation: "226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).
Example 3
Input:s = "06"
Output:0
Explanation: The leading zero makes "06" an invalid code -- only "6" maps to "F" -- so this digit string has no valid decoding, giving a count of 0.

⚖️Formal Constraints & Bounds

  • 1 <= s.length <= 100

  • s contains only digits and may contain leading zero(s).

Deep-Dive & Conceptual Insights

Core Insight

The ways to decode s[index:] depend only on index, never on how the digits before it were split. So dfs(index) is computed once, stored in memo[index], and every other path that reaches the same index reuses it.

Real-World Scenario & Production Applications

Variable-length binary protocol decoding, telephony DTMF sequence parsing.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: One Question per Index

dfs(index) answers: how many ways can s[index:] be decoded? The answer depends on index alone, so it can be stored under index.

Mathematical Recurrence / Code Invariant
if index == len(s):
    return 1
if s[index] == "0":
    return 0

Step-by-Step Execution Trace Table

Code / Blueprint
"226" (Root, d=0, Index 0)
/ \
1-digit ('2') / \ 2-digits ('22')
/ \
"26" (d=1, Index 1) "6" (d=1, Index 2)
/ \ |
1-digit ('2') / 2-digits \ 1-digit ('6') | 1-digit ('6')
/ ('26') \ |
"6" (d=2, Index 2) "" (d=2, Index 3) "" (d=2, Index 3) "" (d=2, Index 3)
| ✅ (1 way) ✅ (1 way) ✅ (1 way)
1-digit ('6')|
"" (d=3, Index 3)
✅ (1 way)
memo[2] = 1 way memo[1] = 1+1 = 2 ways memo[0] = 2+1 = 3 ways
 
Step-by-Step Decision Trace
  1. Step 1 (Root, d=0, Index 0, "226"):
    • Branch 1: Single digit '2' ('B') →\rightarrow→ calls dfs(1).
    • Branch 2: Double digit '22' ('V', 10≤22≤2610 \le 22 \le 2610≤22≤26) →\rightarrow→ calls dfs(2).
  2. Step 2 (Branch 1: Single digit '2', d=1, Index 1, "26"):
    • Branch 1: Single digit '2' ('B') →\rightarrow→ calls dfs(2).
    • Branch 2: Double digit '26' ('Z', 10≤26≤2610 \le 26 \le 2610≤26≤26) →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
  3. Step 3 (Branch 1: Single digit '2', d=2, Index 2, "6"):
    • Single digit '6' ('F') →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
    • Cache memo[2] = 1.
  4. Step 4 (Aggregate Index 1, d=1): dfs(1) = dfs(2) + dfs(3) = 1 + 1 = 2 ways. Cache memo[1] = 2.
  5. Step 5 (Branch 2: Double digit '22', d=1, Index 2, "6"):
    • Single digit '6' ('F') →\rightarrow→ calls dfs(3) →\rightarrow→ Base Case (1 way ✅).
  6. Step 6 (Aggregate Index 0, d=0): dfs(0) = dfs(1) + dfs(2) = 2 + 1 = 3 ways. Cache memo[0] = 3. Record Total Valid Decodings: 3 ("BZ", "VF", "BBF") ✅. Total Valid Decodings: 3 ("BZ", "VF", "BBF").
Code Shape
Archetype Code Shape
Python
1memo = {}
2def dfs(index):
3 if index == len(s):
4 return 1
5 if s[index] == '0':
6 return 0
7 if index in memo:
8 return memo[index]
9 res = dfs(index + 1)
10 if index + 1 < len(s) and (s[index] == '1' or (s[index] == '2' and s[index + 1] <= '6')):
11 res += dfs(index + 2)
12 memo[index] = res
13 return res
14return dfs(0)

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

A message containing letters from A-Z is encoded to numbers using '1' -> 'A', ..., '26' -> 'Z'. Given a string of digits, how many ways can it be decoded? At any index i, the decoder faces a two-way branch:

  1. Single-digit branch: Decode s[i] if s[i] != '0'.
  2. Double-digit branch: Decode s[i:i+2] if the two-digit value is between 10 and 26. Digits like '0' have no individual letter mapping and cannot begin a valid two-digit code (e.g. "06" is invalid). Top-down backtracking with memoization counts the total valid leaves in this branching tree in linear O(N)O(N)O(N) time.
🏟️ The Analogy: The Telegraph Operator Reading Morse Numbers

Imagine a telegraph operator receiving a stream of digits on ticker tape: "226". The operator looks at the first digit: "2" can be 'B'. If the operator decodes "2" as 'B', the remaining tape is "26". But "22" could also be 'V'. If the operator decodes "22" as 'V', the remaining tape is "6". At each step, the operator branches into 1-character or 2-character decodings, dropping invalid sequences (like leading zeros) into the wastebasket.

🔑 Why It Works

Notice the base cases: reaching the end of the string (index == len(s)) contributes 111 valid decoding path, while encountering a standalone '0' immediately aborts the branch with 000 ways. Memoizing by index ensures each digit index is processed at most once.

  • A memo key that misses an argument: memo must be keyed by every argument that changes the answer. Here index alone decides the ways to decode s[index:]; in Interleaving String (LC 97) the answer depends on how far both strings are used, so the key is (i, j), and keying by i alone mixes up calls that used different amounts of s2.

  • Reading a stored 0 as missing: if memo.get(index): treats a stored 0 as absent, so every index before an unreadable "30" is explored again on every call. Test if index in memo:.

  • Returning without storing: memo[index] = res must run before return res, or the cache never fills and the run is as slow as the plain recursion (exponential on a long run of "1"s).

  • Leading zero: '0' has no letter of its own and never starts a two-digit code ('06' is invalid), so dfs returns 0 as soon as s[index] == "0".

  • Two-digit range: only 10 through 26 are codes: s[index] == "1", or s[index] == "2" and s[index + 1] <= "6".

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement asks to "count how many distinct ways" the digits "can be decoded", and says "the same digit string can often be split into letters in more than one way": each position has a one-digit and a two-digit choice, so the plain recursion branches twice per digit and reaches the same index along many different splits. The count for the rest of the string, s[index:], does not depend on how the digits before it were split.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Invariant: memo[index] is written only with the finished number of ways to decode s[index:], so every call after the first one for the same index returns it without exploring again. index is the whole key because it is the only argument that changes the answer.

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Leading zero and range: return 0 when s[index] == "0", and read two digits only for 10 to 26 ("06" and "27" are not codes).

The 60-Second Interview Pitch

Say this out loud before you type a single line

"I'll use Memoization. The plain recursion is easy: at each index I read one digit, or two digits when they form 10 to 26, and add up the ways from what is left. But it reaches the same index along many different splits, so it is exponential. The number of ways to decode the rest of the string depends only on index, not on how I split the digits before it, so I store dfs(index) in memo the first time it finishes and return it on every later call. The trap is the key: it must hold every argument that changes the answer. Here that is index alone. I also test 'index in memo', because a stored zero is a real answer. Each of the N plus one indices is solved once with constant work, so O(N) time, and the memo plus the recursion stack take O(N) space."

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: dfs explores (gets past if index in memo) at most once per value of index, 0 to N, because the first call that finishes writes memo[index] = res and every later call for that index returns at the lookup. An exploring call does constant work (two digit checks, one store) and makes at most two calls, dfs(index + 1) and dfs(index + 2). Each of those calls either explores (at most N + 1 of them in total) or returns in O(1). So there are at most N + 1 exploring calls and 2(N + 1) quick ones: O(N).

SPACE COMPLEXITY

O(N) memo and recursion stack

Look at the allocations: memo holds at most one entry per index that explores, so at most N entries. The recursion goes one level deeper per digit read, so the call stack is at most N + 1 frames deep. The answer is one integer. Total O(N), memo and recursion stack.

Formal Recurrence Relation

T(N) = (N + 1) exploring calls x O(1) + 2(N + 1) lookups x O(1) = O(N)

Derivation Progression

Distinct arguments

index in 0..N -> N + 1 values

The answer of dfs(index) depends on index alone, so there are only N + 1 different questions.

Explore once

memo[index] = res before return res

The first call for an index explores; every later call for it stops at if index in memo and returns the stored value.

Work per exploring call

O(1) + at most 2 calls

Two digit checks, dfs(index + 1), dfs(index + 2) when the two digits form 10 to 26, and one store.

Total

(N + 1) x O(1) + 2(N + 1) x O(1) = O(N)

At most N + 1 calls explore and each makes at most two calls, so at most 3(N + 1) calls run in all, each doing constant work outside its children.

Variable Definitions

NNN

len(s), the number of digits

Memory Architecture & Bounds

🟣 Call Stack

O(N): one frame per digit read, at most N + 1 deep

🔵 Auxiliary Heap

O(N): memo, at most one entry per index

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(1)O(1)O(1): s[0] == "0" returns 0 at once

Average Case

O(N)O(N)O(N)

Worst Case

O(N)O(N)O(N): a string of 1s and 2s, where every index branches twice

State-Space & Subproblem DAG(Directed Acyclic Graph) Topology

State-Space & Subproblem DAG(Directed Acyclic Graph) Topology
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "count how many distinct ways", "can often be split into letters in more than one way". Each digit opens a one-digit and a two-digit branch, and many split paths reach the same index: Memoization, one stored answer per index.

CONSTRAINTS & BOUNDS

1≤s.length≤1001 \le s.length \le 1001≤s.length≤100. The plain recursion makes about 1.6N1.6^N1.6N calls on a run of 1s (317,810 calls for 25 ones); with the memo at most 3(N+1)3(N + 1)3(N+1) calls run. A recursion depth of 101 frames is far below Python's default limit of 1,000.

FAANG PRODUCTION TRAPS & EDGE CASES

Where the cache lives: create memo (or the functools.cache-wrapped dfs) inside numDecodings, so every call starts empty. A module-level cache keeps every earlier string's answers in memory and grows with every call.

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•