Facebook Pixel

3007. Maximum Number That Sum of the Prices Is Less Than or Equal to K

Problem Description

You need to find the greatest "cheap" number based on a special pricing system.

How the pricing works:

  • Given two integers k and x
  • For any number num, its price is calculated by counting set bits (1s) at specific positions in its binary representation
  • The positions checked are: x, 2x, 3x, 4x, ... (multiples of x)
  • Positions are counted from the right, starting at position 1 (least significant bit)

Examples of price calculation:

  • When x = 1 and num = 13 (binary: 1101):

    • Check positions 1, 2, 3, 4, ... from the right
    • Positions 1, 3, and 4 have set bits
    • Price = 3
  • When x = 2 and num = 13 (binary: 1101):

    • Check positions 2, 4, 6, ... from the right
    • Only position 4 has a set bit
    • Price = 1

Accumulated price and cheap numbers:

  • The accumulated price of a number num is the sum of prices of all numbers from 1 to num
  • A number is considered cheap if its accumulated price is less than or equal to k
  • You need to return the greatest cheap number

For example, if k = 10 and x = 2, you need to find the largest number num such that the sum of prices of all numbers from 1 to num is at most 10.

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

Intuition

The key observation is that as numbers increase, their accumulated price (sum of prices from 1 to that number) also increases monotonically. This monotonic property creates a pattern like: true, true, ..., false, false as we increase the number (where true means "accumulated price ≤ k").

We want to find the last true position (maximum cheap number). Using our standard binary search template that finds the first true, we can invert the condition: define feasible(num) as "accumulated price > k" (i.e., the number is too expensive). Then the pattern becomes false, false, ..., true, true and we find the first infeasible number, then subtract 1.

To use binary search, we need to efficiently calculate the accumulated price for any given number num. Computing the price for each individual number from 1 to num would be too slow for large values. This is where digit DP comes in.

Digit DP is perfect for counting problems involving binary representations. We can think of building numbers from 1 to num digit by digit (bit by bit) and count how many times we encounter set bits at positions that are multiples of x. The beauty of digit DP is that it can calculate the total contribution of all numbers up to num in logarithmic time relative to the number of bits.

The approach works by:

  1. Using binary search with the template to find the first num where accumulated price > k
  2. For each candidate mid in binary search, using digit DP to calculate the total count of set bits at positions x, 2x, 3x, etc., across all numbers from 1 to mid
  3. The answer is first_true_index - 1 (the last number that was still cheap)

Learn more about Binary Search and Dynamic Programming patterns.

Solution Implementation

1class Solution:
2    def findMaximumNumber(self, k: int, x: int) -> int:
3        """
4        Find the maximum number where the sum of price bits does not exceed k.
5        Price bits are the bits at positions that are multiples of x.
6        """
7        from functools import cache
8
9        @cache
10        def count_price_bits(bit_position: int, is_limited: bool, price_count: int) -> int:
11            """
12            Count total price bits using digit DP approach.
13            """
14            if bit_position == 0:
15                return price_count
16
17            total_price_bits = 0
18            upper_bound = (self.current_number >> (bit_position - 1) & 1) if is_limited else 1
19
20            for bit_value in range(upper_bound + 1):
21                is_price_bit = (bit_value == 1 and bit_position % x == 0)
22                new_price_count = price_count + (1 if is_price_bit else 0)
23                still_limited = is_limited and (bit_value == upper_bound)
24                total_price_bits += count_price_bits(
25                    bit_position - 1,
26                    still_limited,
27                    new_price_count
28                )
29
30            return total_price_bits
31
32        def is_too_expensive(num: int) -> bool:
33            """Check if accumulated price for numbers 1..num exceeds k."""
34            self.current_number = num
35            total_price = count_price_bits(num.bit_length(), True, 0)
36            count_price_bits.cache_clear()
37            return total_price > k
38
39        # Binary search using template: find first number that is too expensive
40        left, right = 1, 10**18
41        first_true_index = -1
42
43        while left <= right:
44            mid = (left + right) // 2
45
46            if is_too_expensive(mid):
47                first_true_index = mid
48                right = mid - 1
49            else:
50                left = mid + 1
51
52        # Handle edge cases
53        if first_true_index == -1:
54            # All numbers are cheap (shouldn't happen with proper bounds)
55            return 10**18
56        if first_true_index == 1:
57            # Even number 1 is too expensive
58            return 0
59
60        return first_true_index - 1
61
1class Solution {
2    private int x;
3    private long currentNumber;
4    private Long[][] memo;
5
6    /**
7     * Finds the maximum number such that the sum of price values <= k.
8     * Uses standard binary search template: find first number that is too expensive.
9     */
10    public long findMaximumNumber(long k, int x) {
11        this.x = x;
12
13        // Binary search using template: find first number that is too expensive
14        long left = 1;
15        long right = (long) 1e17;
16        long firstTrueIndex = -1;
17
18        while (left <= right) {
19            long mid = left + (right - left) / 2;
20
21            if (isTooExpensive(mid, k)) {
22                firstTrueIndex = mid;
23                right = mid - 1;
24            } else {
25                left = mid + 1;
26            }
27        }
28
29        // Handle edge cases
30        if (firstTrueIndex == -1) {
31            return (long) 1e17;  // All numbers are cheap
32        }
33        if (firstTrueIndex == 1) {
34            return 0;  // Even number 1 is too expensive
35        }
36
37        return firstTrueIndex - 1;
38    }
39
40    /**
41     * Check if accumulated price for numbers 1..num exceeds k.
42     */
43    private boolean isTooExpensive(long num, long k) {
44        currentNumber = num;
45        memo = new Long[65][65];
46        int bitPosition = 64 - Long.numberOfLeadingZeros(num);
47        long totalPriceSum = dfs(bitPosition, 0, true);
48        return totalPriceSum > k;
49    }
50
51    /**
52     * Digit DP to count the sum of price values for all numbers from 1 to currentNumber.
53     */
54    private long dfs(int position, int priceCount, boolean isLimit) {
55        if (position == 0) {
56            return priceCount;
57        }
58
59        if (!isLimit && memo[position][priceCount] != null) {
60            return memo[position][priceCount];
61        }
62
63        long totalSum = 0;
64        int upperBound = isLimit ? (int) ((currentNumber >> (position - 1)) & 1) : 1;
65
66        for (int bitValue = 0; bitValue <= upperBound; ++bitValue) {
67            int newPriceCount = priceCount;
68            if (bitValue == 1 && position % x == 0) {
69                newPriceCount++;
70            }
71            totalSum += dfs(position - 1, newPriceCount, isLimit && bitValue == upperBound);
72        }
73
74        if (!isLimit) {
75            memo[position][priceCount] = totalSum;
76        }
77
78        return totalSum;
79    }
80}
81
1class Solution {
2public:
3    long long findMaximumNumber(long long k, int x) {
4        using ll = long long;
5
6        ll currentNumber = 0;
7        ll dp[65][65];
8
9        // Digit DP function to count special bits
10        auto countSpecialBits = [&](this auto&& countSpecialBits,
11                                    int pos,
12                                    int specialBitCount,
13                                    bool isLimit) -> ll {
14            if (pos == 0) {
15                return specialBitCount;
16            }
17
18            if (!isLimit && dp[pos][specialBitCount] != -1) {
19                return dp[pos][specialBitCount];
20            }
21
22            int upperBound = isLimit ? (currentNumber >> (pos - 1)) & 1 : 1;
23            ll result = 0;
24
25            for (int bit = 0; bit <= upperBound; ++bit) {
26                int newSpecialBitCount = specialBitCount +
27                                        (bit == 1 && pos % x == 0 ? 1 : 0);
28                result += countSpecialBits(pos - 1,
29                                          newSpecialBitCount,
30                                          isLimit && bit == upperBound);
31            }
32
33            if (!isLimit) {
34                dp[pos][specialBitCount] = result;
35            }
36
37            return result;
38        };
39
40        // Lambda: check if num is too expensive (accumulated price > k)
41        auto isTooExpensive = [&](ll num) -> bool {
42            currentNumber = num;
43            memset(dp, -1, sizeof(dp));
44            int bitLength = 64 - __builtin_clzll(num);
45            return countSpecialBits(bitLength, 0, true) > k;
46        };
47
48        // Binary search using template: find first number that is too expensive
49        ll left = 1, right = (ll)1e17;
50        ll firstTrueIndex = -1;
51
52        while (left <= right) {
53            ll mid = left + (right - left) / 2;
54
55            if (isTooExpensive(mid)) {
56                firstTrueIndex = mid;
57                right = mid - 1;
58            } else {
59                left = mid + 1;
60            }
61        }
62
63        // Handle edge cases
64        if (firstTrueIndex == -1) {
65            return (ll)1e17;
66        }
67        if (firstTrueIndex == 1) {
68            return 0;
69        }
70
71        return firstTrueIndex - 1;
72    }
73};
74
1/**
2 * Finds the maximum number such that the sum of price values <= k.
3 * Uses standard binary search template: find first number that is too expensive.
4 */
5function findMaximumNumber(k: number, x: number): number {
6    let currentNumber: bigint;
7
8    const memoTable: bigint[][] = Array.from(
9        { length: 65 },
10        () => Array(65).fill(-1n)
11    );
12
13    const calculatePriceSum = (
14        position: number,
15        setBitCount: number,
16        isLimit: boolean
17    ): bigint => {
18        if (position === 0) {
19            return BigInt(setBitCount);
20        }
21
22        if (!isLimit && memoTable[position][setBitCount] !== -1n) {
23            return memoTable[position][setBitCount];
24        }
25
26        let totalSum: bigint = 0n;
27        let upperBound: number = isLimit
28            ? Number((currentNumber >> BigInt(position - 1)) & 1n)
29            : 1;
30
31        for (let bitValue = 0; bitValue <= upperBound; bitValue++) {
32            let newSetBitCount = setBitCount;
33            if (bitValue === 1 && position % x === 0) {
34                newSetBitCount++;
35            }
36
37            totalSum += calculatePriceSum(
38                position - 1,
39                newSetBitCount,
40                isLimit && bitValue === upperBound
41            );
42        }
43
44        if (!isLimit) {
45            memoTable[position][setBitCount] = totalSum;
46        }
47
48        return totalSum;
49    };
50
51    const isTooExpensive = (num: bigint): boolean => {
52        currentNumber = num;
53        for (let i = 0; i < memoTable.length; i++) {
54            memoTable[i].fill(-1n);
55        }
56        let bitLength = currentNumber.toString(2).length;
57        return calculatePriceSum(bitLength, 0, true) > BigInt(k);
58    };
59
60    // Binary search using template: find first number that is too expensive
61    let left: bigint = 1n;
62    let right: bigint = 10n ** 17n;
63    let firstTrueIndex: bigint = -1n;
64
65    while (left <= right) {
66        let mid: bigint = (left + right) / 2n;
67
68        if (isTooExpensive(mid)) {
69            firstTrueIndex = mid;
70            right = mid - 1n;
71        } else {
72            left = mid + 1n;
73        }
74    }
75
76    // Handle edge cases
77    if (firstTrueIndex === -1n) {
78        return Number(10n ** 17n);
79    }
80    if (firstTrueIndex === 1n) {
81        return 0;
82    }
83
84    return Number(firstTrueIndex - 1n);
85}
86

Solution Approach

The solution uses binary search combined with digit DP to find the greatest cheap number.

Binary Search Template Setup

We use the standard binary search template to find the first number that is too expensive:

left, right = 1, 10**18
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    total_price = calculate_price(mid)  # Using digit DP

    if total_price > k:  # feasible(mid) = "price exceeds k"
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

# first_true_index is the first expensive number
# Answer is first_true_index - 1 (last cheap number)

The feasible(mid) function returns True if the accumulated price > k (too expensive). This inverts the natural condition so we can use the standard "find first true" template.

Digit DP Implementation

The dfs function calculates the total count of valuable bits from all numbers from 1 to self.num:

Parameters:

  • pos: Current bit position being processed (from left to right)
  • limit: Boolean flag indicating if we're still bounded by self.num's digits
  • cnt: Count of valuable bits encountered in the current number being formed

How the DP works:

  1. Base case: When pos == 0, we've processed all bits, return the count cnt

  2. Determine upper bound for current bit:

    • If limit is True: we can only use bits up to what's in self.num at position pos-1
    • If limit is False: we can use either 0 or 1
  3. Recursive transition:

    • Try each valid bit value i (0 or 1)
    • Update count: add 1 if i == 1 and current position is valuable (pos % x == 0)
    • Update limit: remains True only if we're at limit and chose the maximum bit
  4. Memoization: The @cache decorator stores results to avoid recomputation

Final Result

After binary search completes:

  • If first_true_index == 1, even number 1 is too expensive, return 0
  • Otherwise, return first_true_index - 1 as the maximum cheap number

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 a small example with k = 9 and x = 2.

Step 1: Understanding what we're looking for

  • We need to find the largest number whose accumulated price ≤ 9
  • Valuable bit positions are 2, 4, 6, ... (multiples of 2)

Step 2: Binary Search Process

Initial range: l = 1, r = 10^18

First iteration:

  • mid = (1 + 10^18 + 1) >> 1 ≈ 5×10^17
  • This is clearly too large (accumulated price would be huge)
  • Update: r = 5×10^17 - 1

After several iterations, let's focus on when we get closer to the answer:

When checking mid = 7 (binary: 111):

  • Use digit DP to calculate accumulated price from 1 to 7
  • For each number 1-7, count bits at positions 2, 4:
    • 1 (001): position 2 has 0 → price = 0
    • 2 (010): position 2 has 1 → price = 1
    • 3 (011): position 2 has 1 → price = 1
    • 4 (100): position 2 has 0, position 4 has 1 → price = 1
    • 5 (101): position 2 has 0, position 4 has 1 → price = 1
    • 6 (110): position 2 has 1, position 4 has 1 → price = 2
    • 7 (111): position 2 has 1, position 4 has 1 → price = 2
  • Total accumulated price = 0+1+1+1+1+2+2 = 8
  • Since 8 ≤ 9, update l = 7

When checking mid = 8 (binary: 1000):

  • Digit DP calculates accumulated price from 1 to 8
  • Number 8 itself has price = 0 (no bits at positions 2 or 4)
  • Total accumulated price = 8 + 0 = 8
  • Since 8 ≤ 9, update l = 8

When checking mid = 9 (binary: 1001):

  • Number 9 has price = 0 (no bits at positions 2 or 4)
  • Total accumulated price = 8 + 0 = 8
  • Since 8 ≤ 9, update l = 9

When checking mid = 10 (binary: 1010):

  • Number 10 has price = 1 (bit at position 2)
  • Total accumulated price = 8 + 0 + 1 = 9
  • Since 9 ≤ 9, update l = 10

When checking mid = 11 (binary: 1011):

  • Number 11 has price = 1 (bit at position 2)
  • Total accumulated price = 9 + 1 = 10
  • Since 10 > 9, update r = 10

Step 3: Binary search converges

  • Final answer: l = 10

How Digit DP works for calculating accumulated price of 7:

The digit DP builds numbers bit by bit from left to right. For num = 7 (binary: 111):

  1. Start at position 3 (most significant bit)
  2. For each position, decide whether to place 0 or 1
  3. Track if we're still bounded by 7's bits (limit)
  4. Count valuable bits (at positions 2, 4, etc.)

The DP efficiently counts all combinations:

  • Numbers starting with 0__ (1-3): explores all valid combinations
  • Numbers starting with 10_ (4-5): explores combinations
  • Numbers starting with 11_ (6-7): explores remaining combinations

By memoizing intermediate results, we avoid recalculating the same subproblems, making the computation efficient even for large numbers.

Time and Space Complexity

The time complexity is O(log^2 k). This comes from two nested logarithmic operations:

  • The binary search runs O(log k) iterations, as we search for the maximum number in the range [1, 10^18], and the answer is bounded by k
  • For each binary search iteration, the dfs function performs digit DP on a number with at most O(log k) bits (since the maximum valid number has magnitude proportional to k)
  • The dfs function visits each bit position once with at most 2 choices per position (0 or 1), resulting in O(log k) operations per binary search iteration
  • Combined: O(log k) × O(log k) = O(log^2 k)

The space complexity is O(log k). This is determined by:

  • The recursion depth of dfs which is equal to the bit length of the number being processed, at most O(log k)
  • The cache storage for memoization, which stores states for (pos, limit, cnt) tuples. Since pos has O(log k) values, limit has 2 values, and cnt can be at most O(log k), the cache size is bounded by O(log k)
  • The call stack depth during recursion is O(log k)

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

Common Pitfalls

1. Using Wrong Binary Search Template

Pitfall: Not using the standard binary search template with first_true_index tracking.

Common mistakes:

# WRONG: Using while left < right with different update rules
while left < right:
    mid = (left + right + 1) >> 1
    if total_price <= k:
        left = mid
    else:
        right = mid - 1

Correct approach using template:

# CORRECT: Standard template with first_true_index
left, right = 1, 10**18
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if is_too_expensive(mid):  # True when price > k
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index - 1  # Last cheap number

2. Forgetting to Invert the Condition for "Find Maximum"

Pitfall: The template finds the first true position, but we need the last feasible position (maximum cheap number). Must invert the condition.

Wrong:

# WRONG: feasible returns True when number is cheap
def feasible(num):
    return calculate_price(num) <= k  # True = cheap

Correct:

# CORRECT: feasible returns True when number is TOO EXPENSIVE
def is_too_expensive(num):
    return calculate_price(num) > k  # True = too expensive

# Then: first_true_index - 1 = last cheap number

3. Forgetting to Clear the Cache Between Binary Search Iterations

Pitfall: The @cache decorator memoizes results based on function arguments. Since self.current_number changes between iterations but isn't a parameter to the cached function, stale cached values will give incorrect results.

Solution: Always clear the cache after each DP calculation:

total_price = count_price_bits(mid.bit_length(), True, 0)
count_price_bits.cache_clear()  # Essential

4. Off-by-One Error in Bit Position Indexing

Pitfall: Confusing 0-indexed and 1-indexed bit positions.

Correct approach:

# Position is 1-indexed in our implementation
is_price_bit = (bit_value == 1 and bit_position % x == 0)
upper_bound = (self.current_number >> (bit_position - 1) & 1) if is_limited else 1

5. Not Handling Edge Cases

Pitfall: Not handling when first_true_index == 1 (even number 1 is too expensive).

Correct:

if first_true_index == 1:
    return 0  # No cheap numbers exist
return first_true_index - 1
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

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

Load More