Facebook Pixel

3116. Kth Smallest Amount With Single Denomination Combination

HardBit ManipulationArrayMathBinary SearchCombinatoricsNumber Theory
Leetcode Link

Problem Description

You have an integer array coins representing different coin denominations and an integer k. You have an infinite supply of coins of each denomination.

The key constraint is that you cannot combine coins of different denominations - you can only use multiple coins of the same denomination to form amounts.

For example, if coins = [3, 5], you can form:

  • Using only coins of value 3: 3, 6, 9, 12, 15, 18, ...
  • Using only coins of value 5: 5, 10, 15, 20, 25, ...

The valid amounts you can make are: 3, 5, 6, 9, 10, 12, 15, 18, 20, ...

Your task is to find the k-th smallest amount that can be formed following this rule.

For instance, if coins = [3, 5] and k = 4, the first few smallest amounts are 3, 5, 6, 9, so the answer would be 9.

The problem asks you to return this k-th smallest amount as an integer.

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

Intuition

The key insight is to recognize that this problem has a monotonic property: if we can form at least k amounts that are less than or equal to some value x, then we can definitely form at least k amounts for any value greater than x. This monotonicity suggests using binary search.

Instead of directly finding the k-th smallest amount, we can reframe the problem: find the smallest value x such that there are exactly k amounts we can form that are less than or equal to x.

For a given value x, how do we count how many valid amounts are ≤ x?

For a single coin of value a, the amounts we can form are a, 2a, 3a, .... The number of these amounts that are ≤ x is ⌊x/a⌋.

With multiple coins, we might think to just sum up ⌊x/coins[i]⌋ for each coin, but this would overcount. For example, with coins [3, 6], the amount 6 can be formed using either 2×3 or 1×6, but we should only count it once.

This is where the inclusion-exclusion principle comes in. Some amounts can be formed by multiple coins - specifically, amounts that are multiples of the LCM (Least Common Multiple) of those coins.

For coins [a, b]:

  • Add amounts formed by a: ⌊x/a⌋
  • Add amounts formed by b: ⌊x/b⌋
  • Subtract amounts formed by both (multiples of lcm(a,b)): ⌊x/lcm(a,b)⌋

This pattern extends to any number of coins using inclusion-exclusion: add counts for single coins, subtract counts for pairs (to remove duplicates), add back counts for triples (since we over-subtracted), and so on.

Since we have at most 15 coins, we can use bit manipulation to enumerate all possible subsets (2^15 possibilities) and apply inclusion-exclusion to get the exact count of valid amounts ≤ x.

Once we can count amounts for any x, binary search finds the smallest x where the count reaches k.

Learn more about Math, Binary Search and Combinatorics patterns.

Solution Approach

The solution uses binary search combined with the inclusion-exclusion principle to find the k-th smallest amount.

Binary Search Setup: We use bisect_left to find the smallest value x where check(x) returns True. The search range is from 0 to 10^11 (a sufficiently large upper bound).

The check Function: This function determines if there are at least k valid amounts less than or equal to mx.

The implementation uses bit manipulation to enumerate all non-empty subsets of coins:

  • We iterate from 1 to 2^n - 1 where n = len(coins)
  • Each number i represents a subset: if the j-th bit is set, we include coins[j]

Inclusion-Exclusion Implementation: For each subset represented by i:

  1. Calculate LCM of the subset:

    v = 1
    for j, x in enumerate(coins):
        if i >> j & 1:  # Check if j-th bit is set
            v = lcm(v, x)
            if v > mx:  # Optimization: stop if LCM exceeds mx
                break
  2. Apply inclusion-exclusion rule:

    m = i.bit_count()  # Number of coins in this subset
    if m & 1:  # Odd number of elements
        cnt += mx // v  # Add
    else:  # Even number of elements
        cnt -= mx // v  # Subtract
    • For subsets with odd number of elements: add ⌊mx/LCM⌋
    • For subsets with even number of elements: subtract ⌊mx/LCM⌋

Why this works:

  • Single coins (odd size = 1): We add counts like ⌊mx/a⌋, ⌊mx/b⌋
  • Pairs of coins (even size = 2): We subtract overlaps like ⌊mx/lcm(a,b)⌋
  • Triples (odd size = 3): We add back over-corrections like ⌊mx/lcm(a,b,c)⌋
  • And so on...

Final Binary Search: bisect_left(range(10**11), True, key=check) finds the smallest value where check returns True, which is exactly the k-th smallest amount we can form.

The time complexity is O(2^n × log(max_value)) where n is the number of coins (at most 15), making this approach efficient for the given constraints.

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 the 4th smallest amount with coins = [3, 6] and k = 4.

Step 1: Understanding what amounts we can form

  • Using coin 3: 3, 6, 9, 12, 15, ...
  • Using coin 6: 6, 12, 18, 24, ...
  • Combined (sorted): 3, 6, 9, 12, ... (note that 6 appears in both but we count it once)

Step 2: Binary Search Process

We'll search for the smallest value x such that there are at least 4 amounts ≤ x.

Let's trace through some binary search iterations:

Check x = 5:

  • Subsets to consider: {3}, {6}, {3,6}
  • Subset {3} (size=1, odd): Add ⌊5/3⌋ = 1
  • Subset {6} (size=1, odd): Add ⌊5/6⌋ = 0
  • Subset {3,6} (size=2, even): Subtract ⌊5/lcm(3,6)⌋ = ⌊5/6⌋ = 0
  • Total count = 1 + 0 - 0 = 1
  • Since 1 < 4, we need a larger x

Check x = 10:

  • Subset {3} (size=1, odd): Add ⌊10/3⌋ = 3 (amounts: 3, 6, 9)
  • Subset {6} (size=1, odd): Add ⌊10/6⌋ = 1 (amount: 6)
  • Subset {3,6} (size=2, even): Subtract ⌊10/lcm(3,6)⌋ = ⌊10/6⌋ = 1 (overlap: 6)
  • Total count = 3 + 1 - 1 = 3
  • Since 3 < 4, we need a larger x

Check x = 12:

  • Subset {3} (size=1, odd): Add ⌊12/3⌋ = 4 (amounts: 3, 6, 9, 12)
  • Subset {6} (size=1, odd): Add ⌊12/6⌋ = 2 (amounts: 6, 12)
  • Subset {3,6} (size=2, even): Subtract ⌊12/lcm(3,6)⌋ = ⌊12/6⌋ = 2 (overlaps: 6, 12)
  • Total count = 4 + 2 - 2 = 4
  • Since 4 ≥ 4, this satisfies our condition!

Check x = 11:

  • Subset {3} (size=1, odd): Add ⌊11/3⌋ = 3
  • Subset {6} (size=1, odd): Add ⌊11/6⌋ = 1
  • Subset {3,6} (size=2, even): Subtract ⌊11/6⌋ = 1
  • Total count = 3 + 1 - 1 = 3
  • Since 3 < 4, x = 11 is too small

Step 3: Binary Search Conclusion

The binary search finds that x = 12 is the smallest value where we have at least 4 amounts ≤ x.

Therefore, the 4th smallest amount is 12.

We can verify: The amounts in order are 3, 6, 9, 12, and indeed 12 is the 4th one.

Key Insight from the Example: Notice how inclusion-exclusion correctly handled the overlap at 6 and 12:

  • Both values were counted once by coin 3 and once by coin 6
  • The subtraction of lcm(3,6) = 6 removed these duplicates
  • This gave us the exact count of unique amounts

Solution Implementation

1class Solution:
2    def findKthSmallest(self, coins: List[int], k: int) -> int:
3        def count_multiples_up_to(max_value: int) -> bool:
4            """
5            Check if there are at least k multiples of coin values up to max_value.
6            Uses inclusion-exclusion principle to avoid double counting.
7          
8            Args:
9                max_value: The upper bound to check multiples up to
10              
11            Returns:
12                True if count of multiples >= k, False otherwise
13            """
14            total_count = 0
15          
16            # Iterate through all non-empty subsets of coins using bitmask
17            # (1 << len(coins)) gives us 2^n possible subsets
18            for subset_mask in range(1, 1 << len(coins)):
19                lcm_value = 1
20              
21                # Calculate LCM of all coins in current subset
22                for coin_index, coin_value in enumerate(coins):
23                    # Check if coin at coin_index is in current subset
24                    if subset_mask >> coin_index & 1:
25                        lcm_value = lcm(lcm_value, coin_value)
26                        # Early termination if LCM exceeds max_value
27                        if lcm_value > max_value:
28                            break
29              
30                # Count number of set bits (size of current subset)
31                subset_size = subset_mask.bit_count()
32              
33                # Apply inclusion-exclusion principle:
34                # - Add if subset size is odd (include)
35                # - Subtract if subset size is even (exclude to avoid double counting)
36                if subset_size & 1:  # Odd size subset
37                    total_count += max_value // lcm_value
38                else:  # Even size subset
39                    total_count -= max_value // lcm_value
40          
41            return total_count >= k
42      
43        # Binary search for the smallest value where count >= k
44        # Search range: [0, 10^11) should cover all practical cases
45        # bisect_left finds the leftmost position where check returns True
46        return bisect_left(range(10**11), True, key=count_multiples_up_to)
47
1class Solution {
2    private int[] coins;
3    private int k;
4
5    /**
6     * Finds the k-th smallest positive integer that is divisible by at least one coin value.
7     * Uses binary search on the answer combined with inclusion-exclusion principle.
8     * 
9     * @param coins Array of coin values
10     * @param k The k-th position to find
11     * @return The k-th smallest positive integer divisible by at least one coin
12     */
13    public long findKthSmallest(int[] coins, int k) {
14        this.coins = coins;
15        this.k = k;
16      
17        // Binary search range: minimum is 1, maximum is 10^11
18        long left = 1;
19        long right = (long) 1e11;
20      
21        // Binary search for the k-th smallest value
22        while (left < right) {
23            long mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
24          
25            // Check if there are at least k numbers <= mid that are divisible by coins
26            if (check(mid)) {
27                right = mid;  // mid could be the answer, search in lower half
28            } else {
29                left = mid + 1;  // mid is too small, search in upper half
30            }
31        }
32      
33        return left;
34    }
35
36    /**
37     * Counts how many positive integers up to maxValue are divisible by at least one coin.
38     * Uses inclusion-exclusion principle with bitmask to handle overlapping counts.
39     * 
40     * @param maxValue The upper bound to check
41     * @return true if count >= k, false otherwise
42     */
43    private boolean check(long maxValue) {
44        long count = 0;
45        int n = coins.length;
46      
47        // Iterate through all non-empty subsets using bitmask (1 to 2^n - 1)
48        for (int bitmask = 1; bitmask < (1 << n); ++bitmask) {
49            long lcmValue = 1;
50          
51            // Calculate LCM of coins in current subset
52            for (int j = 0; j < n; ++j) {
53                // Check if j-th coin is included in current subset
54                if ((bitmask >> j & 1) == 1) {
55                    lcmValue = lcm(lcmValue, coins[j]);
56                  
57                    // Optimization: if LCM exceeds maxValue, no contribution to count
58                    if (lcmValue > maxValue) {
59                        break;
60                    }
61                }
62            }
63          
64            // Apply inclusion-exclusion principle
65            int subsetSize = Integer.bitCount(bitmask);
66            if (subsetSize % 2 == 1) {
67                // Odd-sized subset: add to count
68                count += maxValue / lcmValue;
69            } else {
70                // Even-sized subset: subtract from count (remove double counting)
71                count -= maxValue / lcmValue;
72            }
73        }
74      
75        return count >= k;
76    }
77
78    /**
79     * Calculates the least common multiple (LCM) of two numbers.
80     * Formula: LCM(a, b) = (a * b) / GCD(a, b)
81     * 
82     * @param a First number
83     * @param b Second number
84     * @return The LCM of a and b
85     */
86    private long lcm(long a, long b) {
87        return a * b / gcd(a, b);
88    }
89
90    /**
91     * Calculates the greatest common divisor (GCD) using Euclidean algorithm.
92     * 
93     * @param a First number
94     * @param b Second number
95     * @return The GCD of a and b
96     */
97    private long gcd(long a, long b) {
98        return b == 0 ? a : gcd(b, a % b);
99    }
100}
101
1class Solution {
2public:
3    long long findKthSmallest(vector<int>& coins, int k) {
4        using ll = long long;
5      
6        // Binary search range: minimum 1, maximum 1e11
7        ll left = 1, right = 1e11;
8        int n = coins.size();
9
10        // Lambda function to count how many multiples exist <= maxValue
11        // Uses inclusion-exclusion principle
12        auto countMultiples = [&](ll maxValue) {
13            ll totalCount = 0;
14          
15            // Iterate through all non-empty subsets of coins (2^n - 1 combinations)
16            for (int mask = 1; mask < (1 << n); ++mask) {
17                ll lcmValue = 1;
18              
19                // Calculate LCM of coins in current subset
20                for (int bitPos = 0; bitPos < n; ++bitPos) {
21                    if ((mask >> bitPos) & 1) {  // Check if bit is set
22                        lcmValue = lcm(lcmValue, static_cast<ll>(coins[bitPos]));
23                        // Early termination if LCM exceeds maxValue
24                        if (lcmValue > maxValue) {
25                            break;
26                        }
27                    }
28                }
29              
30                // Apply inclusion-exclusion principle
31                int subsetSize = __builtin_popcount(mask);
32                if (subsetSize & 1) {  // Odd-sized subset: add
33                    totalCount += maxValue / lcmValue;
34                } else {  // Even-sized subset: subtract
35                    totalCount -= maxValue / lcmValue;
36                }
37            }
38          
39            // Return true if we have at least k multiples
40            return totalCount >= k;
41        };
42
43        // Binary search to find the kth smallest multiple
44        while (left < right) {
45            ll mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
46          
47            if (countMultiples(mid)) {
48                // If mid has >= k multiples, search in left half
49                right = mid;
50            } else {
51                // If mid has < k multiples, search in right half
52                left = mid + 1;
53            }
54        }
55      
56        return left;
57    }
58};
59
1/**
2 * Finds the kth smallest positive integer that is divisible by at least one element in coins array
3 * Uses binary search combined with inclusion-exclusion principle
4 * @param coins - Array of coin denominations
5 * @param k - The position of the smallest number to find
6 * @returns The kth smallest number divisible by at least one coin
7 */
8function findKthSmallest(coins: number[], k: number): number {
9    // Binary search range: from 1 to 10^11
10    let left = 1n;
11    let right = BigInt(1e11);
12    const coinsCount = coins.length;
13  
14    /**
15     * Checks if there are at least k numbers <= maxValue that are divisible by at least one coin
16     * Uses inclusion-exclusion principle to count valid numbers
17     * @param maxValue - The upper bound to check
18     * @returns True if count >= k, false otherwise
19     */
20    const checkIfKthOrLess = (maxValue: bigint): boolean => {
21        let count = 0n;
22      
23        // Iterate through all non-empty subsets of coins (2^n - 1 subsets)
24        for (let subset = 1; subset < (1 << coinsCount); ++subset) {
25            let currentLcm = 1n;
26          
27            // Calculate LCM of all coins in current subset
28            for (let coinIndex = 0; coinIndex < coinsCount; ++coinIndex) {
29                // Check if coin at coinIndex is in current subset
30                if ((subset >> coinIndex) & 1) {
31                    currentLcm = lcm(currentLcm, BigInt(coins[coinIndex]));
32                    // Early termination if LCM exceeds maxValue
33                    if (currentLcm > maxValue) {
34                        break;
35                    }
36                }
37            }
38          
39            // Apply inclusion-exclusion principle
40            const subsetSize = bitCount(subset);
41            if (subsetSize & 1) {
42                // Odd-sized subset: add to count
43                count += maxValue / currentLcm;
44            } else {
45                // Even-sized subset: subtract from count
46                count -= maxValue / currentLcm;
47            }
48        }
49      
50        return count >= BigInt(k);
51    };
52  
53    // Binary search for the exact kth smallest value
54    while (left < right) {
55        const mid = (left + right) >> 1n;
56        if (checkIfKthOrLess(mid)) {
57            right = mid;
58        } else {
59            left = mid + 1n;
60        }
61    }
62  
63    return Number(left);
64}
65
66/**
67 * Calculates the greatest common divisor using Euclidean algorithm
68 * @param a - First number
69 * @param b - Second number
70 * @returns The GCD of a and b
71 */
72function gcd(a: bigint, b: bigint): bigint {
73    return b === 0n ? a : gcd(b, a % b);
74}
75
76/**
77 * Calculates the least common multiple of two numbers
78 * @param a - First number
79 * @param b - Second number
80 * @returns The LCM of a and b
81 */
82function lcm(a: bigint, b: bigint): bigint {
83    return (a * b) / gcd(a, b);
84}
85
86/**
87 * Counts the number of set bits (1s) in the binary representation of a number
88 * Uses Brian Kernighan's algorithm with bit manipulation optimizations
89 * @param i - The number to count bits for
90 * @returns The number of set bits
91 */
92function bitCount(i: number): number {
93    // Remove every second bit
94    i = i - ((i >>> 1) & 0x55555555);
95    // Sum pairs of bits
96    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
97    // Sum nibbles
98    i = (i + (i >>> 4)) & 0x0f0f0f0f;
99    // Sum bytes
100    i = i + (i >>> 8);
101    // Sum 16-bit values
102    i = i + (i >>> 16);
103    // Return only the relevant bits (max 32 bits means max count is 32, which fits in 6 bits)
104    return i & 0x3f;
105}
106

Time and Space Complexity

Time Complexity: O(n × 2^n × log(k × M))

The time complexity consists of three main components:

  1. Binary search range: The binary search is performed on the range [0, 10^11), which takes O(log(10^11)) iterations. Since we're looking for the k-th smallest value that can be formed by multiples of coins, and the maximum coin value is M, the actual search range is approximately O(log(k × M)).

  2. Check function per binary search iteration: For each binary search iteration, the check function is called, which:

    • Iterates through all possible subsets of coins: 2^n subsets (from 1 to 2^n - 1)
    • For each subset, computes the LCM of selected coins, which takes O(n) time in the worst case (iterating through all coins when all bits are set)
    • The LCM calculation itself has a cost, but it's dominated by the iteration through coins
  3. Overall: Combining these factors: O(log(k × M)) binary search iterations × O(2^n) subsets × O(n) operations per subset = O(n × 2^n × log(k × M))

Space Complexity: O(1)

The space complexity is constant as the algorithm only uses:

  • A few integer variables (cnt, v, m, i, j)
  • No additional data structures that scale with input size
  • The recursive calls in bisect_left use O(log(k × M)) stack space, but this is typically considered negligible compared to the dominant factors

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

Common Pitfalls

1. Misunderstanding the Problem Constraint

The most critical pitfall is misinterpreting what amounts can be formed. Many people initially think this is a standard coin change problem where you can mix different denominations (e.g., using one coin of value 3 and one coin of value 5 to make 8). However, the constraint explicitly states you can only use multiple coins of the same denomination for any given amount.

Wrong interpretation: With coins = [3, 5], thinking you can make 8 (3 + 5) Correct interpretation: You cannot make 8 because it requires mixing denominations

2. Incorrect Inclusion-Exclusion Logic

A common mistake is getting the inclusion-exclusion signs backwards:

# WRONG - Signs are reversed!
if subset_size & 1:  # Odd size
    total_count -= max_value // lcm_value  # Should be ADD
else:  # Even size
    total_count += max_value // lcm_value  # Should be SUBTRACT

Solution: Remember that inclusion-exclusion starts with adding single elements (odd-sized subsets) and subtracting their intersections (even-sized subsets).

3. LCM Overflow Not Handled Properly

The LCM can grow very large, potentially exceeding the search bounds:

# WRONG - No overflow check
for coin_index, coin_value in enumerate(coins):
    if subset_mask >> coin_index & 1:
        lcm_value = lcm(lcm_value, coin_value)
        # Missing: if lcm_value > max_value: break

Without the overflow check, you might:

  • Waste computation on LCMs that are already too large
  • Risk integer overflow in extreme cases

Solution: Always check if lcm_value > max_value: break after computing each LCM step.

4. Incorrect Binary Search Bounds

Using too small an upper bound for binary search:

# WRONG - Upper bound too small
return bisect_left(range(10**6), True, key=count_multiples_up_to)

With k potentially being up to 10^9 and coins as small as 1, the k-th smallest value could be quite large.

Solution: Use a sufficiently large upper bound like 10**11 to ensure the answer falls within the search range.

5. Off-by-One Error in Binary Search

Confusing when to use bisect_left vs bisect_right:

# WRONG - Using bisect_right when we need the first valid position
return bisect_right(range(10**11), True, key=count_multiples_up_to)

Since we want the smallest value where the count is at least k, we need bisect_left to find the leftmost position where the condition becomes true.

6. Not Understanding Why This Approach Works

Many might wonder why we're counting multiples using inclusion-exclusion instead of generating and sorting all possible amounts. The key insight is that:

  • We're counting how many valid amounts exist up to a given value
  • Binary search finds the exact value where this count equals k
  • This avoids explicitly generating all amounts, which would be inefficient

Example to clarify: With coins = [3, 5] and checking up to 10:

  • Multiples of 3: {3, 6, 9} → count = 3
  • Multiples of 5: {5, 10} → count = 2
  • Multiples of lcm(3,5)=15: {} → count = 0
  • Total using inclusion-exclusion: 3 + 2 - 0 = 5 valid amounts ≤ 10
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

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

Load More