DSA Digit DP
Digit DP counts numbers up to a bound by building their digits one place at a time. Its key state is a flag that says whether the prefix still matches the bound.
The tight flag in action
The bound is 25. A first digit of 2 stays tight, and a first digit of 1 turns loose.
bound = '25'
for first in ['1', '2']:
tight = first == bound[0]
limit = int(bound[1]) if tight else 9
print('first', first, 'tight', tight, 'next digit up to', limit)Brute force at a small bound
Brute force tests every number directly. It is slow but needs no clever state.
def has_repeat(x):
s = str(x)
return len(set(s)) != len(s)
count = sum(1 for x in range(31) if not has_repeat(x))
print('no repeat up to 30:', count)Leading zeros stay unused
A leading zero starts no digit and marks nothing as used. The first real digit starts the number and sets its bit.
started = False
used = 0
for d in [0, 0, 5, 5]:
if not started and d == 0:
print(d, 'still not started')
continue
started = True
print(d, 'repeat' if used & (1 << d) else 'new digit')
used = used | (1 << d)Example
from functools import lru_cache
def count_digit_dp(limit):
digits = [int(c) for c in str(limit)]
@lru_cache(maxsize=None)
def go(pos, tight, started, used):
if pos == len(digits):
return 1
top = digits[pos] if tight else 9
total = 0
for d in range(top + 1):
ntight = tight and d == top
if not started and d == 0:
total += go(pos + 1, ntight, False, 0)
elif used & (1 << d):
continue
else:
total += go(pos + 1, ntight, True, used | (1 << d))
return total
return go(0, True, False, 0)
def count_brute(limit):
total = 0
for x in range(limit + 1):
s = str(x)
if len(set(s)) == len(s):
total += 1
return total
for limit in (30, 100, 250):
fast = count_digit_dp(limit)
slow = count_brute(limit)
print(limit, 'digit dp', fast, 'brute', slow, 'match', fast == slow)Key points
- A tight flag is true while the built prefix equals the bound prefix
- A loose prefix may place any digit from zero to nine next
- A started flag ignores leading zeros when a property is checked
- The property here is that no digit repeats inside the number
- Brute force checks each number one by one at small bounds
In this example, the digit method counts numbers whose digits never repeat. The number zero counts as valid, because a single digit cannot repeat. Brute force checks every number up to the same bound. The two counts match at all three bounds, which verifies the state design.
Note
Memoisation works on loose states because the bound no longer matters there
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Python Playground
Exercise
What does a tight flag value of true mean?