Facebook Pixel

793. Preimage Size of Factorial Zeroes Function

Problem Description

This problem asks us to find how many non-negative integers have exactly k trailing zeros in their factorial.

First, let's understand what trailing zeros in a factorial mean. The function f(x) counts the number of zeros at the end of x! (x factorial). For example:

  • 3! = 6 has 0 trailing zeros, so f(3) = 0
  • 11! = 39916800 has 2 trailing zeros, so f(11) = 2

Trailing zeros in factorials come from multiplying by 10, which is the product of 2 and 5. Since there are always more factors of 2 than 5 in factorials, the number of trailing zeros equals the number of times 5 appears as a factor in the factorial.

The key insight is that the number of trailing zeros in n! can be calculated by counting how many times 5 appears as a factor in all numbers from 1 to n. This is computed as: n/5 + n/25 + n/125 + ... (using integer division).

Given an integer k, we need to find how many non-negative integers x satisfy f(x) = k. For example:

  • If k = 0, the answer is 5 because f(0) = f(1) = f(2) = f(3) = f(4) = 0
  • If k = 1, the answer is 5 because f(5) = f(6) = f(7) = f(8) = f(9) = 1

The solution uses binary search to find the range of values where f(x) = k. The function g(k) finds the smallest x such that f(x) >= k. The answer is then g(k+1) - g(k), which gives the count of integers with exactly k trailing zeros.

Note that for some values of k, there might be no integers x where f(x) = k, in which case the answer would be 0.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The first observation is that trailing zeros in factorials follow a specific pattern. Since trailing zeros come from factors of 5 (paired with factors of 2), we can calculate f(x) using the formula: x/5 + x/25 + x/125 + .... This gives us a way to compute how many trailing zeros any factorial has.

Next, we notice that f(x) is a non-decreasing function - as x increases, f(x) either stays the same or increases. Moreover, f(x) increases by exactly 1 when x is divisible by 5, by 2 when divisible by 25, and so on. This means consecutive values of x often have the same f(x) value.

For example:

  • f(0) = f(1) = f(2) = f(3) = f(4) = 0
  • f(5) = f(6) = f(7) = f(8) = f(9) = 1
  • f(10) = f(11) = f(12) = f(13) = f(14) = 2

The pattern shows that for most values of k, exactly 5 integers have f(x) = k. However, some values of k are impossible to achieve (like when k would require a number to be divisible by a higher power of 5 than possible).

Since f(x) is monotonic, we can use binary search to find boundaries. We can think of the search space as having a boolean property: for each position x, we ask "does f(x) >= k?" This creates a pattern like [false, false, ..., true, true, ...]. Our goal is to find the first true position - the smallest x where f(x) >= k.

The key insight is to find:

  1. The smallest x where f(x) >= k (let's call this start)
  2. The smallest x where f(x) >= k+1 (let's call this end)

The difference end - start gives us the count of integers with exactly k trailing zeros.

The search space for binary search is bounded by 5*k because if an integer has k trailing zeros, it must be at least around 5*k (since we need at least k factors of 5). This gives us an efficient way to find the boundaries without checking every possible integer.

Learn more about Math and Binary Search patterns.

Solution Implementation

1class Solution:
2    def preimageSizeFZF(self, k: int) -> int:
3        def count_trailing_zeros(n: int) -> int:
4            """Count trailing zeros in n! by counting factors of 5."""
5            if n == 0:
6                return 0
7            return n // 5 + count_trailing_zeros(n // 5)
8
9        def find_first_with_at_least_k_zeros(target_zeros: int) -> int:
10            """
11            Binary search to find smallest n where n! has >= target_zeros trailing zeros.
12            Uses the standard template: find first true in [false, false, ..., true, true, ...]
13            """
14            if target_zeros == 0:
15                return 0
16
17            left, right = 0, 5 * target_zeros
18            first_true_index = -1
19
20            while left <= right:
21                mid = (left + right) // 2
22                if count_trailing_zeros(mid) >= target_zeros:
23                    first_true_index = mid
24                    right = mid - 1
25                else:
26                    left = mid + 1
27
28            return first_true_index if first_true_index != -1 else 5 * target_zeros + 1
29
30        # Count = (first n with >= k+1 zeros) - (first n with >= k zeros)
31        return find_first_with_at_least_k_zeros(k + 1) - find_first_with_at_least_k_zeros(k)
32
1class Solution {
2    public int preimageSizeFZF(int k) {
3        // Count = (first n with >= k+1 zeros) - (first n with >= k zeros)
4        return findFirstWithAtLeastKZeros(k + 1) - findFirstWithAtLeastKZeros(k);
5    }
6
7    /**
8     * Binary search to find smallest n where n! has >= k trailing zeros.
9     * Uses standard template: find first true in [false, false, ..., true, true, ...]
10     */
11    private int findFirstWithAtLeastKZeros(int k) {
12        if (k == 0) {
13            return 0;
14        }
15
16        long left = 0;
17        long right = 5L * k;
18        long firstTrueIndex = -1;
19
20        while (left <= right) {
21            long mid = left + (right - left) / 2;
22            if (countTrailingZeros(mid) >= k) {
23                firstTrueIndex = mid;
24                right = mid - 1;
25            } else {
26                left = mid + 1;
27            }
28        }
29
30        return (int) (firstTrueIndex != -1 ? firstTrueIndex : 5L * k + 1);
31    }
32
33    /** Count trailing zeros in n! by counting factors of 5. */
34    private int countTrailingZeros(long n) {
35        if (n == 0) {
36            return 0;
37        }
38        return (int) (n / 5) + countTrailingZeros(n / 5);
39    }
40}
41
1class Solution {
2public:
3    int preimageSizeFZF(int k) {
4        // Count = (first n with >= k+1 zeros) - (first n with >= k zeros)
5        return findFirstWithAtLeastKZeros(k + 1) - findFirstWithAtLeastKZeros(k);
6    }
7
8private:
9    /**
10     * Binary search to find smallest n where n! has >= k trailing zeros.
11     * Uses standard template: find first true in [false, false, ..., true, true, ...]
12     */
13    int findFirstWithAtLeastKZeros(int k) {
14        if (k == 0) {
15            return 0;
16        }
17
18        long long left = 0;
19        long long right = static_cast<long long>(5) * k;
20        long long firstTrueIndex = -1;
21
22        while (left <= right) {
23            long long mid = left + (right - left) / 2;
24            if (countTrailingZeros(mid) >= k) {
25                firstTrueIndex = mid;
26                right = mid - 1;
27            } else {
28                left = mid + 1;
29            }
30        }
31
32        return static_cast<int>(firstTrueIndex != -1 ? firstTrueIndex : 5LL * k + 1);
33    }
34
35    /** Count trailing zeros in n! by counting factors of 5. */
36    int countTrailingZeros(long long n) {
37        int count = 0;
38        while (n > 0) {
39            n /= 5;
40            count += static_cast<int>(n);
41        }
42        return count;
43    }
44};
45
1function preimageSizeFZF(k: number): number {
2    // Count = (first n with >= k+1 zeros) - (first n with >= k zeros)
3    return findFirstWithAtLeastKZeros(k + 1) - findFirstWithAtLeastKZeros(k);
4}
5
6/**
7 * Binary search to find smallest n where n! has >= k trailing zeros.
8 * Uses standard template: find first true in [false, false, ..., true, true, ...]
9 */
10function findFirstWithAtLeastKZeros(k: number): number {
11    if (k === 0) {
12        return 0;
13    }
14
15    let left = 0;
16    let right = 5 * k;
17    let firstTrueIndex = -1;
18
19    while (left <= right) {
20        const mid = Math.floor((left + right) / 2);
21        if (countTrailingZeros(mid) >= k) {
22            firstTrueIndex = mid;
23            right = mid - 1;
24        } else {
25            left = mid + 1;
26        }
27    }
28
29    return firstTrueIndex !== -1 ? firstTrueIndex : 5 * k + 1;
30}
31
32/** Count trailing zeros in n! by counting factors of 5. */
33function countTrailingZeros(n: number): number {
34    let count = 0;
35    while (n > 0) {
36        n = Math.floor(n / 5);
37        count += n;
38    }
39    return count;
40}
41

Solution Approach

The solution uses the standard binary search template to find boundaries. We need to find the first position where f(x) >= k, which maps perfectly to the "find first true" pattern.

1. Function countTrailingZeros(x): Calculate trailing zeros in x!

def count_trailing_zeros(x):
    if x == 0:
        return 0
    return x // 5 + count_trailing_zeros(x // 5)

This recursive function counts the number of trailing zeros in x! by counting factors of 5. It computes x//5 + x//25 + x//125 + ... through recursion.

2. Function findFirstWithAtLeastKZeros(k): Binary search for first x where f(x) >= k

We use the standard binary search template with first_true_index:

def find_first_with_k_zeros(k):
    left, right = 0, 5 * k
    first_true_index = -1

    while left <= right:
        mid = (left + right) // 2
        if count_trailing_zeros(mid) >= k:  # feasible condition
            first_true_index = mid
            right = mid - 1
        else:
            left = mid + 1

    return first_true_index if first_true_index != -1 else 5 * k + 1

The feasible condition is count_trailing_zeros(mid) >= k. When this is true, mid could be our answer, so we save it and search left for an even smaller value.

3. Main calculation: Count integers with exactly k trailing zeros

return find_first_with_k_zeros(k + 1) - find_first_with_k_zeros(k)

The difference gives us the count of integers x where f(x) = k.

For example, if k = 1:

  • find_first_with_k_zeros(1) = 5 (first number with at least 1 trailing zero)
  • find_first_with_k_zeros(2) = 10 (first number with at least 2 trailing zeros)
  • Answer: 10 - 5 = 5 (the numbers 5, 6, 7, 8, 9 all have exactly 1 trailing zero)

The time complexity is O(log^2(k)) due to the binary search and the recursive calculation of f(x) within it.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through finding how many non-negative integers have exactly k = 2 trailing zeros in their factorial.

Step 1: Understanding what we're looking for We need to find all integers x where x! has exactly 2 trailing zeros. Let's check some values:

  • 9! = 362,880 → 1 trailing zero
  • 10! = 3,628,800 → 2 trailing zeros
  • 11! = 39,916,800 → 2 trailing zeros
  • 14! = 87,178,291,200 → 2 trailing zeros
  • 15! = 1,307,674,368,000 → 3 trailing zeros

So we expect x = 10, 11, 12, 13, 14 to have exactly 2 trailing zeros.

Step 2: Binary search to find first x where f(x) >= 2

Using the template with left = 0, right = 10, first_true_index = -1:

Iterationleftrightmidf(mid)f(mid) >= 2?Action
101051falseleft = 6
261081falseleft = 9
391091falseleft = 10
41010102truefirst_true_index = 10, right = 9

Loop ends (left > right). Result: first_true_index = 10

Step 3: Binary search to find first x where f(x) >= 3

Using the template with left = 0, right = 15, first_true_index = -1:

Iterationleftrightmidf(mid)f(mid) >= 3?Action
101571falseleft = 8
2815112falseleft = 12
31215132falseleft = 14
41415142falseleft = 15
51515153truefirst_true_index = 15, right = 14

Loop ends. Result: first_true_index = 15

Step 4: Calculate the answer Count = 15 - 10 = 5

This means exactly 5 integers (10, 11, 12, 13, 14) have factorials with exactly 2 trailing zeros.

Time and Space Complexity

Time Complexity: O(log^2 k)

The time complexity is determined by the g function which uses bisect_left with a range of size 5k. The bisect_left performs binary search, taking O(log(5k)) = O(log k) iterations. In each iteration of the binary search, the f function is called as the key function.

The f function recursively calculates the number of trailing zeros in x! by counting factors of 5. For a number x, the recursion depth is O(log_5 x). Since x can be at most 5k in our case, the recursion depth is O(log_5(5k)) = O(log k).

Therefore, each call to g(k) has complexity O(log k) * O(log k) = O(log^2 k).

Since we call g twice (with k+1 and k), the overall time complexity remains O(log^2 k).

Space Complexity: O(log k)

The space complexity is dominated by the recursion stack in function f. When f is called with a value up to 5k, the maximum recursion depth is O(log_5(5k)) = O(log k). The bisect_left function uses O(1) additional space as it works with a virtual range and doesn't create the actual list. Therefore, the overall space complexity is O(log k).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using the Wrong Binary Search Template

Pitfall: Using while left < right with right = mid instead of the standard template.

Why it happens: Different binary search variants exist, and developers may mix them up.

Incorrect:

while left < right:
    mid = (left + right) // 2
    if count_trailing_zeros(mid) >= k:
        right = mid
    else:
        left = mid + 1
return left

Correct (using standard template):

first_true_index = -1
while left <= right:
    mid = (left + right) // 2
    if count_trailing_zeros(mid) >= k:
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return first_true_index

2. Incorrect Search Space Upper Bound

Pitfall: Using an insufficient upper bound for the binary search range, such as k or 2*k.

Why it happens: It's tempting to think that if we want k trailing zeros, we only need to search up to around k values. However, since trailing zeros come from factors of 5, and each multiple of 5 contributes at least one factor, we need approximately 5k numbers to get k trailing zeros.

Solution: Always use 5 * k as the upper bound for right.

3. Forgetting Edge Case: k = 0

Pitfall: Not handling the special case where k = 0, which requires finding how many numbers have factorials with zero trailing zeros.

Why it happens: The formula for trailing zeros involves division by 5, and it's easy to overlook that 0!, 1!, 2!, 3!, 4! all have zero trailing zeros.

Solution: Handle k = 0 explicitly by returning 0 immediately in findFirstWithAtLeastKZeros(0).

4. Misunderstanding the Problem: Looking for Exact Match vs Range

Pitfall: Trying to find a single value x where f(x) = k instead of counting all values in the range.

Why it happens: The problem asks for the count of integers, not just whether such an integer exists.

Solution: Remember that the answer is a count. The pattern is that consecutive integers often have the same number of trailing zeros in their factorials (e.g., 5!, 6!, 7!, 8!, 9! all have exactly 1 trailing zero). The solution correctly uses the difference to count all such integers.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More