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

Builds on Linear DP and Digit Manipulation, which you haven’t finished yet.

P3HardMental model~6 min speedrun

Number of Digit One (LC 233)(opens leetcode.com)

Add up how many times the digit 1 is written across all the whole numbers from 0 to n, both ends included. A number with several 1s, such as 11, counts each of them.

Example 1
Input:n = 13
Output:6
The 1s are in 1, 10, 11 (two of them), 12 and 13: 6 in all.
10101112123134two 1s

Digit DP

limit n ➔ fill digits left to right with a tight flag ➔ count
Spot it when: You must count how many numbers up to a huge limit n (up to 10^9 or more) share some digit property: how many contain a 1, how many can be written from a given digit set, how many have no repeated digit. Checking each number one by one is far too slow.
Digit DP
Python
1from functools import lru_cache
2 
3def count_digit_one(n: int) -> int:
4 """Number of Digit One: digit DP over the digits of n, keeping whether the prefix still equals n's.
5 Time: O(D^2 * 10) | Space: O(D^2)
6 """
7 # digits: n's digits, most significant first; size is how many digit positions the walk fills
8 digits = [int(c) for c in str(n)]
9 size = len(digits)
10 
11 @lru_cache(maxsize=None)
12 def count(pos: int, ones: int, tight: bool) -> int:
13 """The 1s in every number that finishes this prefix, which already holds `ones` 1s."""
14 # 1. Finish A Number:
15 # Every digit is placed; the finished number contributes the 1 digits it already holds.
16 if pos == size:
17 return ones
18 # 2. Cap This Digit While Tight:
19 # tight means the prefix still equals n's digits, so this digit may not pass n's digit here.
20 top = digits[pos] if tight else 9
21 total = 0
22 for d in range(top + 1):
23 # 3. Place Digit d And Stay Tight Only On n's Own Digit:
24 # A digit below top makes every later digit free (0-9), so tight must carry forward only
25 # when it was already tight and d matches n's digit exactly here.
26 total += count(pos + 1, ones + (d == 1), tight and d == top)
27 return total
28 
29 return count(0, 0, True)
Invariant: count(pos, ones, tight) is the number of 1s in every number that finishes a prefix of length pos holding ones 1s so far, and tight is true exactly when that prefix still equals n's digits.

Build every number up to n digit by digit, from the most significant digit, and keep one bit of history about the limit: tight, whether the digits placed so far equal n's. While tight, the next digit may not pass n's digit there; once a smaller digit has been placed, every later digit is free from 0 to 9. Add whatever extra fact the question needs to the state, here ones, the count of 1 digits placed so far, and memoize on the whole state: count(pos, ones, tight). This is correct because two prefixes with the same (position, extra fact, tight) finish in exactly the same set of ways, so each state needs solving only once, and the base case at pos == size reads off the answer for a finished number. The trap is the tight bit: the next position stays tight only when tight and d == top, never d == top alone. Dropping the tight and part re-caps a number that is already smaller than n, undercounting it.

See it run

Digit DP for n = 13, step by step

pos 0 · ones 0 · tight True

pos 1 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 1 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 1 · total 1

pos 1 · ones 1 · tight True

pos 2 · ones 1 · tight False

pos 2 · ones 2 · tight False

pos 2 · ones 1 · tight False

pos 2 · ones 1 · tight True

pos 1 · total 5

pos 0 · total 6

answer 6

Each column is a digit position, and the last column is a finished number. The digit row is the digit placed. In the tight row, T means the prefix still equals the start of n. The ones row counts the 1s placed.

  • top the digit that caps a tight call
  • call the current call
  • cached an answer taken from lru_cache
  • base a finished number, which returns ones
  • return the total returned by a call
  • answer the final result

count(0, 0, True) has tight = True, so top = digits[0] = 1. The digit d runs from 0 to 1.

Place d = 0 at position 0. count(1, 0, False) has tight = False, so top = 9. The digit d runs from 0 to 9.

Place d = 0 at position 1. count(2, 0, False) has pos == size, so the number is finished. It returns ones = 0.

Place d = 1 at position 1. count(2, 1, False) has pos == size, so the number is finished. It returns ones = 1.

Place d = 2 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 3 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 4 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 5 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 6 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 7 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 8 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 9 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

count(1, 0, False) adds the results of its 10 digits. It returns total = 1.

Place d = 1 at position 0. count(1, 1, True) has tight = True, so top = digits[1] = 3. The digit d runs from 0 to 3.

Place d = 0 at position 1. count(2, 1, False) was computed before. So lru_cache returns 1 at once.

Place d = 1 at position 1. count(2, 2, False) has pos == size, so the number is finished. It returns ones = 2.

Place d = 2 at position 1. count(2, 1, False) was computed before. So lru_cache returns 1 at once.

Place d = 3 at position 1. count(2, 1, True) has pos == size, so the number is finished. It returns ones = 1.

count(1, 1, True) adds the results of its 4 digits. It returns total = 5.

count(0, 0, True) adds the results of its 2 digits. It returns total = 6.

count(0, 0, True) returned 6. This is the number of 1 digits written in all numbers from 0 to 13.

n
pos 01top
pos 13
end
digit
tight
Tcall
ones
0
1 / 21
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing d == top instead of tight and d == top

    Mistake
    total += count(pos + 1, ones + (d == 1), d == top)
    Fix
    total += count(pos + 1, ones + (d == 1), tight and d == top)

    What happens: Once a digit below n's digit is placed, the number is already smaller than n. Every later digit is then free from 0 to 9. With d == top alone, a later 9 sets tight to True again. The next digit is then capped at n's digit, and some numbers are never counted.

    Example: n = 100
    Mistake: returns 20Fix: returns 21
  2. 2

    Gotcha 2: Writing top = 9 without checking tight

    Mistake
    top = 9
    Fix
    top = digits[pos] if tight else 9

    What happens: While tight is True, the digits placed so far equal n's digits. The next digit may not be larger than digits[pos]. With top = 9, the walk also builds numbers larger than n and counts their 1s.

    Example: n = 13
    Mistake: returns 20Fix: returns 6
  3. 3

    Gotcha 3: Leaving ones out of the memo key

    Mistake
    memo[(pos, tight)] = total
    Fix
    memo[(pos, ones, tight)] = total

    What happens: The base case returns ones, so the total depends on ones. A key of (pos, tight) treats two prefixes with different counts of 1s as the same state. The second prefix then gets the total stored for the first one.

    Example: n = 20
    Mistake: returns 2Fix: returns 12
  4. 4

    Gotcha 4: Counting a range [lo, hi] with one call

    Mistake
    count_digit_one(hi)
    Fix
    count_digit_one(hi) - count_digit_one(lo - 1)

    What happens: The walk counts every number from 0 up to its limit. It has no lower limit. For a range from lo to hi, count up to hi and subtract the count up to lo - 1.

    Example: lo = 10, hi = 13
    Mistake: returns 6Fix: returns 5
Not this one if…
  • You read or rebuild the digits of one given number, as in Palindrome Number (LC 9): nothing is counted over every number up to a limit, so there is no tight flag and no memo, only a digit loop. See Digit Manipulation

Deep Dive

What tight means and why one bit is enough

The walk builds a number from its most significant digit. We want every number from 0 to n. A number is allowed only if it is not larger than n.

While the digits placed so far equal the first digits of n, the next digit may not pass digits[pos]. This is the tight case. Once a placed digit is smaller than n's digit at that position, the number is already smaller than n. Every later digit is free from 0 to 9, whatever n says.

So the whole history of the prefix is reduced to one bit: is it still equal to n's prefix? The next tight value is tight and d == top. It stays True only when the walk was tight and the digit equals the top.

Why two prefixes can share one answer

Two different prefixes with the same pos, the same ones and the same tight have the same possible endings. The digits still to place, and the limit on each, depend only on pos and tight. The number of 1s still to count is the same, and the 1s already placed are in ones.

So count(pos, ones, tight) returns the same value for both, and lru_cache stores it once. This replaces a search over all numbers up to n by a search over states.

Example: n = 13, so digits = [1, 3]. The call count(0, 0, True) places 0 or 1 at position 0. Placing 0 leads to count(1, 0, False), which counts the numbers 0 to 9 and returns 1. Placing 1 leads to count(1, 1, True), which places 0 to 3 and returns 1 + 2 + 1 + 1 = 5. The total is 1 + 5 = 6. The 1s in 1, 10, 11, 12 and 13 are 1, 1, 2, 1 and 1, which also give 6.

The cost: states times work per state

Let D be the number of digits of n. pos has D + 1 values, ones has at most D + 1 values, and tight has 2. So there are about 2 * (D + 1) * (D + 1) states at most. Each state loops over at most 10 digits. The time is O(D^2 * 10) and the space is O(D^2).

Example: for n = 1000000000, D = 10, and the bound is 242 states. A run of the card's code creates 66 states and returns 900000001. Going through each number from 0 to n would need one billion and one steps.

The cost grows with the number of digits, not with the value of n. This is why the method works for a limit as large as 10^9 or more.

Choosing what goes into the state

The state holds the extra fact that the question needs. For Number of Digit One (LC 233), the fact is ones, the count of 1 digits so far.

For Numbers At Most N Given Digit Set (LC 902), the fact is whether the number has started. For Numbers With Repeated Digits (LC 1012), the fact is the set of digits used, written as a bitmask.

In the card's code, leading zeros are placed as the digit 0, so each number is padded with zeros on the left. A zero does not change ones, so the padding is harmless here. If a zero matters, for example when you count distinct digits, add a started flag to the state.

60-Second Pitch

"Checking each number up to n takes n steps, which is too slow for 10^9. So I build numbers digit by digit from the left and count them in groups. The state is (pos, ones, tight). tight says whether the prefix still equals n's prefix. While tight, the next digit is at most digits[pos]. After a smaller digit, every later digit is free, from 0 to 9. The next tight value is tight and d == top. Two prefixes with the same state have the same endings, so I memoize with lru_cache. There are about D squared states with 10 digits to try each. That gives O(D^2 * 10) time, which is a few hundred operations for a limit of 10^9."

Common Questions

Why is the tight flag needed?

Without it, the walk cannot know whether the next digit may be larger than n's digit. While the prefix equals n's prefix, a larger digit would create a number above n. After a smaller digit, any digit is allowed. One boolean stores this fact, and the number of states stays small.

What is the time and space complexity?

O(D^2 * 10) time and O(D^2) space, where D is the number of digits of n. There are about 2 * (D + 1) * (D + 1) states and each tries at most 10 digits. It depends on the number of digits, so it grows with the logarithm of n.

How do you count numbers in a range [a, b]?

Compute the answer for b and subtract the answer for a - 1. Both calls use the same function. If a is 0, the second count is 0.

When do you need a started flag?

You need it when a leading zero changes the answer. In the card's code, numbers are padded with zeros on the left, and a 0 does not change the count of 1s. For a rule such as no repeated digit, 007 would be treated as having a repeated 0, so you add started, which becomes True when the first non-zero digit is placed.

How do you solve Numbers At Most N Given Digit Set (LC 902) or Numbers With Repeated Digits (LC 1012) with this card?

Keep the same walk over n's digits and the same tight flag. Change the digits that may be placed: only the given set in LC 902, or any digit in LC 1012. Change the extra fact: started for LC 902, and a bitmask of used digits for LC 1012. For LC 1012, it is often easier to count the numbers with no repeated digit and subtract from n.

SWE Reasoning

Spot it

  • The question asks how many numbers up to a limit n have a digit property, and n is as large as 10^9 or more.
  • Checking every number from 0 to n is too slow, but the number of digits is small, about 10 to 19.
  • The property can be decided one digit at a time, with a small amount of memory about the digits placed so far.
  • The question uses words such as "contains a digit", "no repeated digit", "digit sum" or "written with these digits".
  • A range [a, b] is asked: count up to b and subtract the count up to a - 1.

The invariant, step by step

  1. digits holds n's digits from the most significant one, and size is their number.
  2. count(pos, ones, tight) is called. The prefix has pos digits, it contains ones 1s, and tight says whether it equals n's first pos digits.
  3. If pos == size, the number is finished and the function returns ones.
  4. Otherwise it sets top = digits[pos] when tight, or 9 when not tight. A digit above top would make the number larger than n.
  5. For each d from 0 to top, it adds count(pos + 1, ones + (d == 1), tight and d == top).
  6. The new tight is True only if the old one was True and d equals top. Any smaller digit frees every later position.
  7. The result of the first call, count(0, 0, True), is the number of 1s in all numbers from 0 to n.

Traps

  • d == top without tight. After a smaller digit is placed, a later digit equal to n's digit sets tight to True again, and the next digit is capped. Some numbers are not counted. For n = 100, the result is 20 instead of 21.
  • top = 9 while tight. The walk builds numbers larger than n. For n = 13, the result is 20 instead of 6.
  • Missing ones in the state or in the key. If the count is not part of the call, two prefixes with different counts share one stored value, and the result is wrong.
  • Leading zeros that matter. A property such as distinct digits treats 007 differently from 7. Add a started flag, or the padded zeros are counted as digits.
  • Off by one on a range. To count in [a, b], compute the count for b and subtract the count for a - 1. Using a removes a itself, which is wrong.

Practice

Canonical LeetCode:Number of Digit One (LC 233)Numbers At Most N Given Digit Set (LC 902)Numbers With Repeated Digits (LC 1012)Count Special Integers (LC 2376)
Theory Context•Dynamic Programming

Builds on Linear DP and Digit Manipulation, which you haven’t finished yet.

P3HardMental model~6 min speedrun

Number of Digit One (LC 233)(opens leetcode.com)

Add up how many times the digit 1 is written across all the whole numbers from 0 to n, both ends included. A number with several 1s, such as 11, counts each of them.

Example 1
Input:n = 13
Output:6
The 1s are in 1, 10, 11 (two of them), 12 and 13: 6 in all.
10101112123134two 1s

Digit DP

limit n ➔ fill digits left to right with a tight flag ➔ count
Spot it when: You must count how many numbers up to a huge limit n (up to 10^9 or more) share some digit property: how many contain a 1, how many can be written from a given digit set, how many have no repeated digit. Checking each number one by one is far too slow.
Digit DP
Python
1from functools import lru_cache
2 
3def count_digit_one(n: int) -> int:
4 """Number of Digit One: digit DP over the digits of n, keeping whether the prefix still equals n's.
5 Time: O(D^2 * 10) | Space: O(D^2)
6 """
7 # digits: n's digits, most significant first; size is how many digit positions the walk fills
8 digits = [int(c) for c in str(n)]
9 size = len(digits)
10 
11 @lru_cache(maxsize=None)
12 def count(pos: int, ones: int, tight: bool) -> int:
13 """The 1s in every number that finishes this prefix, which already holds `ones` 1s."""
14 # 1. Finish A Number:
15 # Every digit is placed; the finished number contributes the 1 digits it already holds.
16 if pos == size:
17 return ones
18 # 2. Cap This Digit While Tight:
19 # tight means the prefix still equals n's digits, so this digit may not pass n's digit here.
20 top = digits[pos] if tight else 9
21 total = 0
22 for d in range(top + 1):
23 # 3. Place Digit d And Stay Tight Only On n's Own Digit:
24 # A digit below top makes every later digit free (0-9), so tight must carry forward only
25 # when it was already tight and d matches n's digit exactly here.
26 total += count(pos + 1, ones + (d == 1), tight and d == top)
27 return total
28 
29 return count(0, 0, True)
Invariant: count(pos, ones, tight) is the number of 1s in every number that finishes a prefix of length pos holding ones 1s so far, and tight is true exactly when that prefix still equals n's digits.

Build every number up to n digit by digit, from the most significant digit, and keep one bit of history about the limit: tight, whether the digits placed so far equal n's. While tight, the next digit may not pass n's digit there; once a smaller digit has been placed, every later digit is free from 0 to 9. Add whatever extra fact the question needs to the state, here ones, the count of 1 digits placed so far, and memoize on the whole state: count(pos, ones, tight). This is correct because two prefixes with the same (position, extra fact, tight) finish in exactly the same set of ways, so each state needs solving only once, and the base case at pos == size reads off the answer for a finished number. The trap is the tight bit: the next position stays tight only when tight and d == top, never d == top alone. Dropping the tight and part re-caps a number that is already smaller than n, undercounting it.

See it run

Digit DP for n = 13, step by step

pos 0 · ones 0 · tight True

pos 1 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 1 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 2 · ones 0 · tight False

pos 1 · total 1

pos 1 · ones 1 · tight True

pos 2 · ones 1 · tight False

pos 2 · ones 2 · tight False

pos 2 · ones 1 · tight False

pos 2 · ones 1 · tight True

pos 1 · total 5

pos 0 · total 6

answer 6

Each column is a digit position, and the last column is a finished number. The digit row is the digit placed. In the tight row, T means the prefix still equals the start of n. The ones row counts the 1s placed.

  • top the digit that caps a tight call
  • call the current call
  • cached an answer taken from lru_cache
  • base a finished number, which returns ones
  • return the total returned by a call
  • answer the final result

count(0, 0, True) has tight = True, so top = digits[0] = 1. The digit d runs from 0 to 1.

Place d = 0 at position 0. count(1, 0, False) has tight = False, so top = 9. The digit d runs from 0 to 9.

Place d = 0 at position 1. count(2, 0, False) has pos == size, so the number is finished. It returns ones = 0.

Place d = 1 at position 1. count(2, 1, False) has pos == size, so the number is finished. It returns ones = 1.

Place d = 2 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 3 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 4 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 5 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 6 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 7 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 8 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

Place d = 9 at position 1. count(2, 0, False) was computed before. So lru_cache returns 0 at once.

count(1, 0, False) adds the results of its 10 digits. It returns total = 1.

Place d = 1 at position 0. count(1, 1, True) has tight = True, so top = digits[1] = 3. The digit d runs from 0 to 3.

Place d = 0 at position 1. count(2, 1, False) was computed before. So lru_cache returns 1 at once.

Place d = 1 at position 1. count(2, 2, False) has pos == size, so the number is finished. It returns ones = 2.

Place d = 2 at position 1. count(2, 1, False) was computed before. So lru_cache returns 1 at once.

Place d = 3 at position 1. count(2, 1, True) has pos == size, so the number is finished. It returns ones = 1.

count(1, 1, True) adds the results of its 4 digits. It returns total = 5.

count(0, 0, True) adds the results of its 2 digits. It returns total = 6.

count(0, 0, True) returned 6. This is the number of 1 digits written in all numbers from 0 to 13.

n
pos 01top
pos 13
end
digit
tight
Tcall
ones
0
1 / 21
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing d == top instead of tight and d == top

    Mistake
    total += count(pos + 1, ones + (d == 1), d == top)
    Fix
    total += count(pos + 1, ones + (d == 1), tight and d == top)

    What happens: Once a digit below n's digit is placed, the number is already smaller than n. Every later digit is then free from 0 to 9. With d == top alone, a later 9 sets tight to True again. The next digit is then capped at n's digit, and some numbers are never counted.

    Example: n = 100
    Mistake: returns 20Fix: returns 21
  2. 2

    Gotcha 2: Writing top = 9 without checking tight

    Mistake
    top = 9
    Fix
    top = digits[pos] if tight else 9

    What happens: While tight is True, the digits placed so far equal n's digits. The next digit may not be larger than digits[pos]. With top = 9, the walk also builds numbers larger than n and counts their 1s.

    Example: n = 13
    Mistake: returns 20Fix: returns 6
  3. 3

    Gotcha 3: Leaving ones out of the memo key

    Mistake
    memo[(pos, tight)] = total
    Fix
    memo[(pos, ones, tight)] = total

    What happens: The base case returns ones, so the total depends on ones. A key of (pos, tight) treats two prefixes with different counts of 1s as the same state. The second prefix then gets the total stored for the first one.

    Example: n = 20
    Mistake: returns 2Fix: returns 12
  4. 4

    Gotcha 4: Counting a range [lo, hi] with one call

    Mistake
    count_digit_one(hi)
    Fix
    count_digit_one(hi) - count_digit_one(lo - 1)

    What happens: The walk counts every number from 0 up to its limit. It has no lower limit. For a range from lo to hi, count up to hi and subtract the count up to lo - 1.

    Example: lo = 10, hi = 13
    Mistake: returns 6Fix: returns 5
Not this one if…
  • You read or rebuild the digits of one given number, as in Palindrome Number (LC 9): nothing is counted over every number up to a limit, so there is no tight flag and no memo, only a digit loop. See Digit Manipulation

Deep Dive

What tight means and why one bit is enough

The walk builds a number from its most significant digit. We want every number from 0 to n. A number is allowed only if it is not larger than n.

While the digits placed so far equal the first digits of n, the next digit may not pass digits[pos]. This is the tight case. Once a placed digit is smaller than n's digit at that position, the number is already smaller than n. Every later digit is free from 0 to 9, whatever n says.

So the whole history of the prefix is reduced to one bit: is it still equal to n's prefix? The next tight value is tight and d == top. It stays True only when the walk was tight and the digit equals the top.

Why two prefixes can share one answer

Two different prefixes with the same pos, the same ones and the same tight have the same possible endings. The digits still to place, and the limit on each, depend only on pos and tight. The number of 1s still to count is the same, and the 1s already placed are in ones.

So count(pos, ones, tight) returns the same value for both, and lru_cache stores it once. This replaces a search over all numbers up to n by a search over states.

Example: n = 13, so digits = [1, 3]. The call count(0, 0, True) places 0 or 1 at position 0. Placing 0 leads to count(1, 0, False), which counts the numbers 0 to 9 and returns 1. Placing 1 leads to count(1, 1, True), which places 0 to 3 and returns 1 + 2 + 1 + 1 = 5. The total is 1 + 5 = 6. The 1s in 1, 10, 11, 12 and 13 are 1, 1, 2, 1 and 1, which also give 6.

The cost: states times work per state

Let D be the number of digits of n. pos has D + 1 values, ones has at most D + 1 values, and tight has 2. So there are about 2 * (D + 1) * (D + 1) states at most. Each state loops over at most 10 digits. The time is O(D^2 * 10) and the space is O(D^2).

Example: for n = 1000000000, D = 10, and the bound is 242 states. A run of the card's code creates 66 states and returns 900000001. Going through each number from 0 to n would need one billion and one steps.

The cost grows with the number of digits, not with the value of n. This is why the method works for a limit as large as 10^9 or more.

Choosing what goes into the state

The state holds the extra fact that the question needs. For Number of Digit One (LC 233), the fact is ones, the count of 1 digits so far.

For Numbers At Most N Given Digit Set (LC 902), the fact is whether the number has started. For Numbers With Repeated Digits (LC 1012), the fact is the set of digits used, written as a bitmask.

In the card's code, leading zeros are placed as the digit 0, so each number is padded with zeros on the left. A zero does not change ones, so the padding is harmless here. If a zero matters, for example when you count distinct digits, add a started flag to the state.

60-Second Pitch

"Checking each number up to n takes n steps, which is too slow for 10^9. So I build numbers digit by digit from the left and count them in groups. The state is (pos, ones, tight). tight says whether the prefix still equals n's prefix. While tight, the next digit is at most digits[pos]. After a smaller digit, every later digit is free, from 0 to 9. The next tight value is tight and d == top. Two prefixes with the same state have the same endings, so I memoize with lru_cache. There are about D squared states with 10 digits to try each. That gives O(D^2 * 10) time, which is a few hundred operations for a limit of 10^9."

Common Questions

Why is the tight flag needed?

Without it, the walk cannot know whether the next digit may be larger than n's digit. While the prefix equals n's prefix, a larger digit would create a number above n. After a smaller digit, any digit is allowed. One boolean stores this fact, and the number of states stays small.

What is the time and space complexity?

O(D^2 * 10) time and O(D^2) space, where D is the number of digits of n. There are about 2 * (D + 1) * (D + 1) states and each tries at most 10 digits. It depends on the number of digits, so it grows with the logarithm of n.

How do you count numbers in a range [a, b]?

Compute the answer for b and subtract the answer for a - 1. Both calls use the same function. If a is 0, the second count is 0.

When do you need a started flag?

You need it when a leading zero changes the answer. In the card's code, numbers are padded with zeros on the left, and a 0 does not change the count of 1s. For a rule such as no repeated digit, 007 would be treated as having a repeated 0, so you add started, which becomes True when the first non-zero digit is placed.

How do you solve Numbers At Most N Given Digit Set (LC 902) or Numbers With Repeated Digits (LC 1012) with this card?

Keep the same walk over n's digits and the same tight flag. Change the digits that may be placed: only the given set in LC 902, or any digit in LC 1012. Change the extra fact: started for LC 902, and a bitmask of used digits for LC 1012. For LC 1012, it is often easier to count the numbers with no repeated digit and subtract from n.

SWE Reasoning

Spot it

  • The question asks how many numbers up to a limit n have a digit property, and n is as large as 10^9 or more.
  • Checking every number from 0 to n is too slow, but the number of digits is small, about 10 to 19.
  • The property can be decided one digit at a time, with a small amount of memory about the digits placed so far.
  • The question uses words such as "contains a digit", "no repeated digit", "digit sum" or "written with these digits".
  • A range [a, b] is asked: count up to b and subtract the count up to a - 1.

The invariant, step by step

  1. digits holds n's digits from the most significant one, and size is their number.
  2. count(pos, ones, tight) is called. The prefix has pos digits, it contains ones 1s, and tight says whether it equals n's first pos digits.
  3. If pos == size, the number is finished and the function returns ones.
  4. Otherwise it sets top = digits[pos] when tight, or 9 when not tight. A digit above top would make the number larger than n.
  5. For each d from 0 to top, it adds count(pos + 1, ones + (d == 1), tight and d == top).
  6. The new tight is True only if the old one was True and d equals top. Any smaller digit frees every later position.
  7. The result of the first call, count(0, 0, True), is the number of 1s in all numbers from 0 to n.

Traps

  • d == top without tight. After a smaller digit is placed, a later digit equal to n's digit sets tight to True again, and the next digit is capped. Some numbers are not counted. For n = 100, the result is 20 instead of 21.
  • top = 9 while tight. The walk builds numbers larger than n. For n = 13, the result is 20 instead of 6.
  • Missing ones in the state or in the key. If the count is not part of the call, two prefixes with different counts share one stored value, and the result is wrong.
  • Leading zeros that matter. A property such as distinct digits treats 007 differently from 7. Add a started flag, or the padded zeros are counted as digits.
  • Off by one on a range. To count in [a, b], compute the count for b and subtract the count for a - 1. Using a removes a itself, which is wrong.

Practice

Canonical LeetCode:Number of Digit One (LC 233)Numbers At Most N Given Digit Set (LC 902)Numbers With Repeated Digits (LC 1012)Count Special Integers (LC 2376)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•