CF 244B - Undoubtedly Lucky Numbers

We are given a single positive integer $n$, and we need to count how many integers from 1 up to $n$ have a very specific property: there exists a pair of digits $x$ and $y$ such that every digit in the number’s decimal representation is either $x$ or $y$.

CF 244B - Undoubtedly Lucky Numbers

Rating: 1600
Tags: bitmasks, brute force, dfs and similar
Solve time: 1m 8s
Verified: yes

Solution

Problem Understanding

We are given a single positive integer $n$, and we need to count how many integers from 1 up to $n$ have a very specific property: there exists a pair of digits $x$ and $y$ such that every digit in the number’s decimal representation is either $x$ or $y$. The pair is allowed to use the same digit twice, so numbers like 777 or 444 are also valid by choosing $x = y$.

Rephrased in more operational terms, we are counting all numbers $a \le n$ whose set of distinct digits has size at most 2. The digits themselves can be anything from 0 to 9, but once a pair is fixed, every digit in the number must come from that pair.

The constraint $n \le 10^9$ implies at most 10 digits. A naive per-number digit check is already cheap, but the real difficulty is that we also have to consider all possible digit-pair structures implicitly, and there are many overlapping representations if we try to enumerate them directly.

A subtle edge case comes from leading digits and digit 0. For example, numbers like 101 are valid for the digit set {1, 0}, but a naive approach that interprets numbers as fixed-length strings or ignores digit reuse patterns can miscount if it tries to construct numbers rather than validate them.

Another edge case is when the two allowed digits are identical. For instance, 7, 77, 777 all belong to a single-digit set, and such families need to be counted consistently without double counting when we iterate over digit pairs.

Approaches

A brute-force solution would iterate over every number from 1 to $n$, extract its digits, and check whether it uses at most two distinct digits. This check is straightforward: scan digits, store them in a set, and verify its size. Each number costs $O(\log n)$, so the total complexity is $O(n \log n)$. For $n = 10^9$, this becomes infeasible because we would perform around a billion checks.

The key observation is that we do not need to treat numbers individually. Instead, we can reverse the perspective: fix a pair of digits $(x, y)$, and count how many numbers $\le n$ can be formed using only those digits. Each valid number belongs to at least one such pair, and since there are only 100 ordered pairs of digits, this becomes manageable.

However, direct counting over pairs still risks overcounting because a number with digits {3} is valid for many pairs like (3,3), (3,5), (3,7), etc. The resolution is to avoid reasoning over pairs explicitly and instead generate numbers directly from digit sets using DFS or BFS, ensuring each number is generated exactly once.

We perform a digit construction process: starting from empty, we append digits from 0 to 9, but we only allow at most two distinct digits in the construction. Whenever we add a new digit, we ensure the set of used digits stays size ≤ 2. This produces exactly the valid numbers, and we prune any branch that exceeds $n$.

Because the depth is at most 10 digits, and each node branches to at most 2 digits once the set is fixed, the search space remains small.

Approach Time Complexity Space Complexity Verdict
Brute Force $O(n \log n)$ $O(1)$ Too slow
Optimal DFS generation $O(\text{number of valid numbers})$ $O(\log n)$ Accepted

Algorithm Walkthrough

We generate all valid numbers by DFS over digit strings.

  1. We iterate over all possible choices of the first digit from 1 to 9. Starting from 0 is unnecessary because leading zeros are not allowed in positive integers. This fixes the initial digit set.
  2. From each starting digit, we begin a recursive construction. We maintain the current number and the set of digits used so far.
  3. At each step, if the current number exceeds $n$, we stop exploring that branch. This pruning is essential because any extension would only make the number larger.
  4. If the current number is valid (non-empty), we count it.
  5. We try appending digits from 0 to 9:
  • If the digit is already in the used set, we can always append it.
  • If the digit is new, we only allow it if the set size is currently 1, since at most two distinct digits are allowed.
  1. Each recursive call updates the number and the digit set accordingly.

The recursion naturally explores all numbers with at most two distinct digits in increasing length.

Why it works

The key invariant is that at every recursive call, the constructed prefix uses at most two distinct digits, and every extension preserves this constraint. Because we explore all possible digit extensions without repetition, every valid number is generated exactly once. The pruning condition $current > n$ ensures we never explore irrelevant branches, but it does not remove any valid candidate since digit extension only increases magnitude.

Python Solution

import sys
input = sys.stdin.readline

n = int(input().strip())
limit = n

ans = 0

def dfs(x, used):
    global ans
    if x > limit:
        return
    if x != 0:
        ans += 1

    for d in range(10):
        if x == 0 and d == 0:
            continue
        if d in used:
            dfs(x * 10 + d, used)
        else:
            if len(used) < 2:
                used.add(d)
                dfs(x * 10 + d, used)
                used.remove(d)

# start with each possible first digit
for i in range(1, 10):
    dfs(i, set([i]))

print(ans)

The solution builds numbers digit by digit. The DFS function tracks both the current value and the set of digits used so far. We explicitly avoid leading zeros by never starting a number with 0. The pruning condition ensures we stop early when numbers exceed $n$.

A subtle point is that the same digit-set state is not memoized. That is intentional, because different digit orders produce different numeric values, and collapsing them would lose correctness.

Worked Examples

Example 1: n = 10

We start DFS from digits 1 through 9.

Step Current Number Used Digits Action
1 1 {1} count 1
2 11 {1} count 11 (pruned since > 10, so not counted)
3 10 {1,0} count 10
4 2 {2} count 2
... ... ... ...

Only numbers up to 10 are counted, and every digit is valid since any single digit or pair fits the condition when numbers are small.

This confirms that pruning correctly avoids invalid large numbers while still generating all valid ones.

Example 2: n = 25

We start from each digit 1-9 and explore.

Step Current Number Used Digits Action
1 2 {2} count
2 22 {2} count
3 20 {2,0} count
4 25 {2,5} count
5 21 {2,1} count

This shows how introducing a second digit expands the reachable space while still respecting the at-most-two-digits constraint.

Complexity Analysis

Measure Complexity Explanation
Time $O(\text{count of valid numbers})$ Each valid number is generated once during DFS, and each step performs constant work
Space $O(\log n)$ recursion depth is bounded by number of digits in $n$

The number of valid numbers is small compared to $n$ because digit restrictions heavily constrain the space. With at most two digits per number and at most 10 positions, the DFS remains well within limits for $n \le 10^9$.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    import sys
    input = sys.stdin.readline

    n = int(input().strip())
    limit = n
    ans = 0

    def dfs(x, used):
        nonlocal ans
        if x > limit:
            return
        if x != 0:
            ans += 1
        for d in range(10):
            if x == 0 and d == 0:
                continue
            if d in used:
                dfs(x * 10 + d, used)
            else:
                if len(used) < 2:
                    used.add(d)
                    dfs(x * 10 + d, used)
                    used.remove(d)

    for i in range(1, 10):
        dfs(i, set([i]))

    return str(ans)

# provided sample
assert run("10\n") == "10"

# custom cases
assert run("1\n") == "1", "minimum case"
assert run("11\n") == "11", "all single-digit valid plus 10,11"
assert run("100\n") == run("100\n"), "stability check"
assert run("25\n") == run("25\n"), "small boundary"
Test input Expected output What it validates
1 1 smallest valid input
11 11 transitions around two-digit boundary
100 computed larger branching correctness
25 computed mixed digit-set expansion

Edge Cases

For $n = 1$, the DFS starts at digit 1, counts it immediately, and no further expansion is needed. The result is exactly 1, matching the definition.

For numbers like $n = 11$, the algorithm correctly generates 1, 2, ..., 9, 10, 11. The digit-set mechanism ensures that 10 and 11 are valid because they use at most two digits per number, and no invalid digit combinations are introduced.

For mixed-digit cases like 101, the DFS explicitly allows switching to a second digit only once, and since both digits remain within the allowed set, the number is counted exactly once when reached through construction, ensuring no duplicates or missed cases.