LeetCode 2801 - Count Stepping Numbers in Range
The problem asks us to count how many integers in the inclusive range [low, high] are stepping numbers. A stepping number is defined as a number where every pair of adjacent digits differs by exactly 1.
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming
Solution
Problem Understanding
The problem asks us to count how many integers in the inclusive range [low, high] are stepping numbers.
A stepping number is defined as a number where every pair of adjacent digits differs by exactly 1. For example:
123454321is a stepping number because every adjacent pair differs by110is a stepping number because|1 - 0| = 198is a stepping number because|9 - 8| = 111is not a stepping number because|1 - 1| = 0135is not a stepping number because|1 - 3| = 2
The input numbers are given as strings instead of integers because they can be extremely large, up to 10^100. That means normal integer iteration is impossible. We cannot simply loop from low to high and test each number individually.
The task is to return the number of valid stepping numbers within the range, modulo 10^9 + 7.
The constraints immediately suggest that brute force enumeration is infeasible:
highcan contain up to100digits- The numeric range may therefore be astronomically large
- We need a digit-based dynamic programming solution instead of explicit enumeration
A few important observations and edge cases stand out immediately:
- Numbers cannot contain leading zeros
- Single-digit numbers are always stepping numbers because there are no adjacent digits to violate the rule
- The range boundaries themselves may or may not be stepping numbers
- We must correctly handle ranges spanning different digit lengths
- Very large inputs require string-based digit processing rather than numeric arithmetic
The core challenge is counting valid numbers efficiently without generating every integer in the range.
Approaches
Brute Force Approach
The most direct approach would be:
- Convert
lowandhighinto integers - Iterate through every number in the range
- Check whether each number is a stepping number
- Count the valid ones
To verify whether a number is stepping:
- Convert it into digits
- Compare every adjacent pair
- Ensure every absolute difference equals
1
This approach is correct because it explicitly validates every number in the range. However, it is completely impractical for the given constraints.
If high contains up to 100 digits, the range size could be close to 10^100, which is computationally impossible to iterate through.
Optimal Approach, Digit DP
The key insight is that we do not need to enumerate every number individually.
Instead, we can build valid numbers digit by digit while enforcing the stepping constraint during construction.
This naturally leads to Digit Dynamic Programming, often called Digit DP.
The idea is:
- Count how many stepping numbers are less than or equal to a given bound
- Then compute:
$$\text{count}(high) - \text{count}(low - 1)$$
To count valid numbers up to a limit:
-
Process the number from left to right
-
At each digit position:
-
Decide which digit to place
-
Respect the upper bound constraint
-
Respect the stepping-number adjacency rule
-
Use memoization to avoid recomputing identical states
The DP state needs to track:
- Current digit index
- Previous digit chosen
- Whether we are still limited by the upper bound
- Whether we have started building a non-leading-zero number
Because the number length is at most 100, the total number of DP states is very small.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((high - low) × D) | O(D) | Explicitly checks every number |
| Optimal | O(D × 10 × 2 × 2 × 11) | O(D × 10 × 2 × 2) | Uses Digit DP with memoization |
Here, D is the number of digits, at most 100.
Algorithm Walkthrough
Step 1: Create a helper function count(limit)
We first define a function that counts how many stepping numbers are less than or equal to a given string limit.
This transforms the original range problem into:
$$\text{answer} = \text{count}(high) - \text{count}(low - 1)$$
Step 2: Use digit DP
We process the digits from left to right.
At each position, we decide which digit to place.
The recursive DP state contains:
index
- Current digit position
prev_digit
- Previously chosen digit
- Needed to verify the stepping condition
tight
- Whether the current prefix exactly matches the limit
- If true, the next digit cannot exceed the corresponding digit in the limit
started
- Whether we have placed a non-leading-zero digit yet
- Needed because leading zeros should not participate in the stepping constraint
Step 3: Handle leading zeros carefully
Before the number starts:
- We may continue placing zeros
- These zeros are treated as "empty positions"
- They do not trigger the stepping condition
Once the first non-zero digit is placed:
- Every subsequent digit must differ from the previous digit by exactly
1
Step 4: Enforce the stepping constraint
If the number has already started:
- The next digit is valid only if:
$$|digit - prev_digit| = 1$$
This guarantees the final number is a stepping number.
Step 5: Memoize DP states
Many recursive states repeat.
For example:
- Same position
- Same previous digit
- Same tight state
- Same started state
will always produce the same result.
Memoization reduces exponential recursion into a small polynomial number of states.
Step 6: Compute low - 1
To count numbers inside [low, high], we compute:
$$\text{count}(high) - \text{count}(low - 1)$$
Since inputs are strings, we implement manual string subtraction.
Step 7: Return modulo 10^9 + 7
All intermediate results are computed modulo:
$$10^9 + 7$$
to avoid overflow and satisfy the problem requirement.
Why it works
The DP explores every valid number digit by digit while pruning invalid choices immediately.
The tight flag guarantees we never exceed the bound. The started flag correctly handles leading zeros. The adjacency check guarantees every constructed number satisfies the stepping property.
Because every valid number corresponds to exactly one DP path, and every DP path constructs exactly one valid number, the algorithm counts all valid stepping numbers exactly once.
Python Solution
from functools import lru_cache
MOD = 10**9 + 7
class Solution:
def countSteppingNumbers(self, low: str, high: str) -> int:
def subtract_one(num: str) -> str:
if num == "0":
return "0"
digits = list(num)
i = len(digits) - 1
while i >= 0:
if digits[i] != '0':
digits[i] = str(int(digits[i]) - 1)
break
digits[i] = '9'
i -= 1
result = ''.join(digits).lstrip('0')
return result if result else "0"
def count_up_to(limit: str) -> int:
digits = list(map(int, limit))
n = len(digits)
@lru_cache(None)
def dp(index: int, prev_digit: int, tight: bool, started: bool) -> int:
if index == n:
return 1 if started else 0
upper = digits[index] if tight else 9
total = 0
for digit in range(upper + 1):
next_tight = tight and (digit == upper)
if not started:
if digit == 0:
total += dp(
index + 1,
-1,
next_tight,
False
)
else:
total += dp(
index + 1,
digit,
next_tight,
True
)
else:
if abs(digit - prev_digit) == 1:
total += dp(
index + 1,
digit,
next_tight,
True
)
return total % MOD
return dp(0, -1, True, False)
low_minus_one = subtract_one(low)
result = (
count_up_to(high)
- count_up_to(low_minus_one)
) % MOD
return result
The implementation begins with the subtract_one helper, which decrements a numeric string without converting it into a large integer. This is necessary because the input length can reach 100 digits.
The main logic resides inside count_up_to. This function counts all stepping numbers less than or equal to a given limit.
The recursive DP function processes the number one digit at a time.
The index parameter tracks the current position. The prev_digit parameter stores the previously chosen digit so we can verify the stepping constraint. The tight flag ensures we never exceed the upper bound. The started flag distinguishes between leading zeros and actual digits.
If we have not started building the number yet, we may either:
- Continue skipping digits with leading zeros
- Start the number with a non-zero digit
Once the number has started, every next digit must differ from the previous digit by exactly 1.
Memoization via lru_cache prevents recomputation of repeated states.
Finally, we compute:
count(high) - count(low - 1)
which gives the number of stepping numbers inside the inclusive range.
Go Solution
package main
const MOD int = 1000000007
func countSteppingNumbers(low string, high string) int {
subtractOne := func(num string) string {
if num == "0" {
return "0"
}
digits := []byte(num)
i := len(digits) - 1
for i >= 0 {
if digits[i] != '0' {
digits[i]--
break
}
digits[i] = '9'
i--
}
start := 0
for start < len(digits) && digits[start] == '0' {
start++
}
if start == len(digits) {
return "0"
}
return string(digits[start:])
}
var countUpTo func(string) int
countUpTo = func(limit string) int {
digits := []byte(limit)
n := len(digits)
type State struct {
index int
prevDigit int
tight int
started int
}
memo := map[State]int{}
var dp func(int, int, int, int) int
dp = func(index int, prevDigit int, tight int, started int) int {
if index == n {
if started == 1 {
return 1
}
return 0
}
state := State{
index,
prevDigit,
tight,
started,
}
if tight == 0 {
if val, exists := memo[state]; exists {
return val
}
}
upper := 9
if tight == 1 {
upper = int(digits[index] - '0')
}
total := 0
for digit := 0; digit <= upper; digit++ {
nextTight := 0
if tight == 1 && digit == upper {
nextTight = 1
}
if started == 0 {
if digit == 0 {
total += dp(
index+1,
-1,
nextTight,
0,
)
} else {
total += dp(
index+1,
digit,
nextTight,
1,
)
}
} else {
if abs(digit-prevDigit) == 1 {
total += dp(
index+1,
digit,
nextTight,
1,
)
}
}
total %= MOD
}
if tight == 0 {
memo[state] = total
}
return total
}
return dp(0, -1, 1, 0)
}
lowMinusOne := subtractOne(low)
result := (countUpTo(high) - countUpTo(lowMinusOne)) % MOD
if result < 0 {
result += MOD
}
return result
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
The Go implementation follows the same logic as the Python version but uses explicit memoization with a map because Go does not provide built-in memoization decorators like lru_cache.
The DP state is represented with a struct so it can be used as a map key efficiently.
Go also requires careful handling of negative modulo results. After subtraction, we add MOD if the result becomes negative.
String manipulation uses byte slices for efficiency.
Worked Examples
Example 1
Input:
low = "1"
high = "11"
We compute:
count(11) - count(0)
Counting stepping numbers up to 11
| Number | Stepping? | Reason |
|---|---|---|
| 1 | Yes | Single digit |
| 2 | Yes | Single digit |
| 3 | Yes | Single digit |
| 4 | Yes | Single digit |
| 5 | Yes | Single digit |
| 6 | Yes | Single digit |
| 7 | Yes | Single digit |
| 8 | Yes | Single digit |
| 9 | Yes | Single digit |
| 10 | Yes | |1 - 0| = 1 |
| 11 | No | |1 - 1| = 0 |
Total:
10
DP Trace
At index 0:
| Chosen Digit | Started | Valid |
|---|---|---|
| 0 | No | Continue |
| 1 | Yes | Allowed |
From prefix 1:
| Next Digit | Difference | Valid |
|---|---|---|
| 0 | 1 | Yes |
| 1 | 0 | No |
Valid constructed numbers:
1
10
Combined with all single-digit numbers, total becomes 10.
Example 2
Input:
low = "90"
high = "101"
We compute:
count(101) - count(89)
Stepping numbers inside range
| Number | Valid? | Reason |
|---|---|---|
| 98 | Yes | |9 - 8| = 1 |
| 101 | Yes | |1 - 0| = 1 and |0 - 1| = 1 |
Answer:
2
DP Construction for 101
Starting with digit 1:
| Current Prefix | Next Allowed Digits |
|---|---|
| 1 | 0, 2 |
| 10 | 1 |
| 101 | Complete valid number |
The DP successfully builds 101.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(D × 10 × 11 × 2 × 2) | Each DP state tries at most 10 digits |
| Space | O(D × 11 × 2 × 2) | Memoization table size |
Here, D is the number of digits in the bound, at most 100.
The number of DP states is very small because:
indexranges over at most100prev_digithas only11possibilities including-1tighthas2statesstartedhas2states
Each state explores at most 10 digit choices.
This makes the solution extremely efficient even for 100 digit inputs.
Test Cases
sol = Solution()
assert sol.countSteppingNumbers("1", "11") == 10 # example 1
assert sol.countSteppingNumbers("90", "101") == 2 # example 2
assert sol.countSteppingNumbers("1", "1") == 1 # single digit
assert sol.countSteppingNumbers("2", "2") == 1 # another single digit
assert sol.countSteppingNumbers("10", "10") == 1 # valid two-digit stepping
assert sol.countSteppingNumbers("11", "11") == 0 # invalid two-digit number
assert sol.countSteppingNumbers("98", "98") == 1 # valid descending step
assert sol.countSteppingNumbers("99", "99") == 0 # equal adjacent digits
assert sol.countSteppingNumbers("100", "100") == 0 # 1->0 valid, 0->0 invalid
assert sol.countSteppingNumbers("101", "101") == 1 # valid three-digit stepping
assert sol.countSteppingNumbers("0", "0") == 0 # no positive stepping number
assert sol.countSteppingNumbers("1", "9") == 9 # all single digits valid
assert sol.countSteppingNumbers("10", "15") == 2 # only 10 and 12
assert sol.countSteppingNumbers("20", "25") == 2 # 21 and 23
assert sol.countSteppingNumbers("1000", "1100") > 0 # larger range
assert sol.countSteppingNumbers("1", "100000") > 0 # stress test
Test Summary
| Test | Why |
|---|---|
"1", "11" |
Verifies sample case |
"90", "101" |
Verifies multi-digit transitions |
"1", "1" |
Smallest positive range |
"11", "11" |
Invalid repeated digits |
"98", "98" |
Descending stepping number |
"100", "100" |
Fails because of repeated zero |
"101", "101" |
Valid alternating pattern |
"1", "9" |
All single digits valid |
"10", "15" |
Mixed valid and invalid numbers |
"1000", "1100" |
Larger digit lengths |
"1", "100000" |
Stress test for DP |
Edge Cases
One important edge case is single-digit numbers. Every single-digit number is automatically a stepping number because there are no adjacent digits to compare. A buggy implementation might incorrectly reject them by assuming at least one adjacency check is required. The DP handles this naturally because once a non-zero digit is placed and the recursion ends, the number is counted as valid.
Another important edge case involves leading zeros. During digit construction, the DP may temporarily place zeros before the number officially starts. These zeros must not participate in the stepping constraint. For example, while constructing 101, the internal representation may pass through prefixes like 001. The started flag ensures leading zeros are ignored until the first non-zero digit appears.
A third tricky edge case occurs when computing low - 1. Since inputs are strings and may contain many digits, normal integer subtraction is impossible. Cases like "1000" require borrow propagation to produce "999". The helper function performs manual subtraction digit by digit and removes leading zeros correctly.
Another subtle edge case is repeated zeros inside the number. For example, 100 is not a stepping number because:
|1 - 0| = 1
|0 - 0| = 0
The adjacency check inside the DP guarantees these invalid sequences are rejected immediately.
Finally, extremely large bounds such as 10^100 would overflow normal numeric types in most languages. Because the solution processes numbers as strings and works digit by digit, it handles arbitrarily large inputs safely and efficiently.