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 Implementation

1class Solution:
2    def findKthSmallest(self, coins: List[int], k: int) -> int:
3        def count_multiples_up_to(max_value: int) -> int:
4            """
5            Count how many valid amounts can be formed 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                Count of valid amounts <= max_value
13            """
14            total_count = 0
15
16            # Iterate through all non-empty subsets of coins using bitmask
17            for subset_mask in range(1, 1 << len(coins)):
18                lcm_value = 1
19
20                # Calculate LCM of all coins in current subset
21                for coin_index, coin_value in enumerate(coins):
22                    if subset_mask >> coin_index & 1:
23                        lcm_value = lcm(lcm_value, coin_value)
24                        if lcm_value > max_value:
25                            break
26
27                # Apply inclusion-exclusion principle
28                subset_size = subset_mask.bit_count()
29                if subset_size & 1:
30                    total_count += max_value // lcm_value
31                else:
32                    total_count -= max_value // lcm_value
33
34            return total_count
35
36        def feasible(mid: int) -> bool:
37            """Check if there are at least k valid amounts <= mid."""
38            return count_multiples_up_to(mid) >= k
39
40        # Binary search using the standard template
41        left, right = 1, 10**11
42        first_true_index = -1
43
44        while left <= right:
45            mid = (left + right) // 2
46            if feasible(mid):
47                first_true_index = mid
48                right = mid - 1
49            else:
50                left = mid + 1
51
52        return first_true_index
53
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    public long findKthSmallest(int[] coins, int k) {
10        this.coins = coins;
11        this.k = k;
12
13        // Binary search using the standard template
14        long left = 1;
15        long right = (long) 1e11;
16        long firstTrueIndex = -1;
17
18        while (left <= right) {
19            long mid = left + (right - left) / 2;
20            if (feasible(mid)) {
21                firstTrueIndex = mid;
22                right = mid - 1;
23            } else {
24                left = mid + 1;
25            }
26        }
27
28        return firstTrueIndex;
29    }
30
31    /**
32     * Check if there are at least k valid amounts <= maxValue.
33     */
34    private boolean feasible(long maxValue) {
35        return countMultiples(maxValue) >= k;
36    }
37
38    /**
39     * Counts how many positive integers up to maxValue are divisible by at least one coin.
40     * Uses inclusion-exclusion principle with bitmask.
41     */
42    private long countMultiples(long maxValue) {
43        long count = 0;
44        int n = coins.length;
45
46        for (int bitmask = 1; bitmask < (1 << n); ++bitmask) {
47            long lcmValue = 1;
48
49            for (int j = 0; j < n; ++j) {
50                if ((bitmask >> j & 1) == 1) {
51                    lcmValue = lcm(lcmValue, coins[j]);
52                    if (lcmValue > maxValue) {
53                        break;
54                    }
55                }
56            }
57
58            int subsetSize = Integer.bitCount(bitmask);
59            if (subsetSize % 2 == 1) {
60                count += maxValue / lcmValue;
61            } else {
62                count -= maxValue / lcmValue;
63            }
64        }
65
66        return count;
67    }
68
69    private long lcm(long a, long b) {
70        return a * b / gcd(a, b);
71    }
72
73    private long gcd(long a, long b) {
74        return b == 0 ? a : gcd(b, a % b);
75    }
76}
77
1class Solution {
2public:
3    long long findKthSmallest(vector<int>& coins, int k) {
4        using ll = long long;
5        int n = coins.size();
6
7        // Count how many valid amounts exist <= maxValue
8        auto countMultiples = [&](ll maxValue) -> ll {
9            ll totalCount = 0;
10
11            for (int mask = 1; mask < (1 << n); ++mask) {
12                ll lcmValue = 1;
13
14                for (int bitPos = 0; bitPos < n; ++bitPos) {
15                    if ((mask >> bitPos) & 1) {
16                        lcmValue = lcm(lcmValue, static_cast<ll>(coins[bitPos]));
17                        if (lcmValue > maxValue) {
18                            break;
19                        }
20                    }
21                }
22
23                int subsetSize = __builtin_popcount(mask);
24                if (subsetSize & 1) {
25                    totalCount += maxValue / lcmValue;
26                } else {
27                    totalCount -= maxValue / lcmValue;
28                }
29            }
30
31            return totalCount;
32        };
33
34        // Check if there are at least k valid amounts <= mid
35        auto feasible = [&](ll mid) -> bool {
36            return countMultiples(mid) >= k;
37        };
38
39        // Binary search using the standard template
40        ll left = 1;
41        ll right = 1e11;
42        ll firstTrueIndex = -1;
43
44        while (left <= right) {
45            ll mid = left + (right - left) / 2;
46            if (feasible(mid)) {
47                firstTrueIndex = mid;
48                right = mid - 1;
49            } else {
50                left = mid + 1;
51            }
52        }
53
54        return firstTrueIndex;
55    }
56};
57
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 */
5function findKthSmallest(coins: number[], k: number): number {
6    const coinsCount = coins.length;
7
8    /**
9     * Count how many valid amounts exist <= maxValue.
10     */
11    const countMultiples = (maxValue: bigint): bigint => {
12        let count = 0n;
13
14        for (let subset = 1; subset < (1 << coinsCount); ++subset) {
15            let currentLcm = 1n;
16
17            for (let coinIndex = 0; coinIndex < coinsCount; ++coinIndex) {
18                if ((subset >> coinIndex) & 1) {
19                    currentLcm = lcm(currentLcm, BigInt(coins[coinIndex]));
20                    if (currentLcm > maxValue) {
21                        break;
22                    }
23                }
24            }
25
26            const subsetSize = bitCount(subset);
27            if (subsetSize & 1) {
28                count += maxValue / currentLcm;
29            } else {
30                count -= maxValue / currentLcm;
31            }
32        }
33
34        return count;
35    };
36
37    /**
38     * Check if there are at least k valid amounts <= mid.
39     */
40    const feasible = (mid: bigint): boolean => {
41        return countMultiples(mid) >= BigInt(k);
42    };
43
44    // Binary search using the standard template
45    let left = 1n;
46    let right = BigInt(1e11);
47    let firstTrueIndex = -1n;
48
49    while (left <= right) {
50        const mid = (left + right) / 2n;
51        if (feasible(mid)) {
52            firstTrueIndex = mid;
53            right = mid - 1n;
54        } else {
55            left = mid + 1n;
56        }
57    }
58
59    return Number(firstTrueIndex);
60}
61
62function gcd(a: bigint, b: bigint): bigint {
63    return b === 0n ? a : gcd(b, a % b);
64}
65
66function lcm(a: bigint, b: bigint): bigint {
67    return (a * b) / gcd(a, b);
68}
69
70function bitCount(i: number): number {
71    i = i - ((i >>> 1) & 0x55555555);
72    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
73    i = (i + (i >>> 4)) & 0x0f0f0f0f;
74    i = i + (i >>> 8);
75    i = i + (i >>> 16);
76    return i & 0x3f;
77}
78

Solution Approach

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

Binary Search Template: We use the standard binary search template to find the smallest value where at least k valid amounts exist:

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

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index

The feasible(mid) function returns True if there are at least k valid amounts ≤ mid.

Counting Valid Amounts: For a given value mid, we count how many valid amounts exist using inclusion-exclusion:

  1. Enumerate all non-empty subsets of coins using bitmask (from 1 to 2^n - 1)

  2. Calculate LCM of each subset:

    lcm_value = 1
    for coin_index, coin_value in enumerate(coins):
        if subset_mask >> coin_index & 1:
            lcm_value = lcm(lcm_value, coin_value)
            if lcm_value > mid:
                break
  3. Apply inclusion-exclusion:

    • For subsets with odd number of elements: add mid // lcm_value
    • For subsets with even number of elements: subtract mid // lcm_value

Why Inclusion-Exclusion Works:

  • Single coins (size 1): Add counts like ⌊mid/a⌋, ⌊mid/b⌋
  • Pairs (size 2): Subtract overlaps like ⌊mid/lcm(a,b)⌋
  • Triples (size 3): Add back over-corrections like ⌊mid/lcm(a,b,c)⌋

Feasible Function Pattern: The feasible function creates a boolean pattern over the search space:

false, false, false, ..., true, true, true, ...

We find the first true position, which is exactly the k-th smallest amount.

The time complexity is O(2^n × log(max_value)) where n is the number of coins (at most 15).

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

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. Using Wrong Binary Search Template Variant

A critical mistake is using the while left < right with right = mid pattern instead of the standard template:

# WRONG - Non-standard template variant
while left < right:
    mid = (left + right) // 2
    if feasible(mid):
        right = mid
    else:
        left = mid + 1
return left

Correct approach using the standard template:

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

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index

The standard template explicitly tracks the answer with first_true_index, uses while left <= right, and always moves bounds by exactly 1 (right = mid - 1).

2. Misunderstanding the Problem Constraint

Many people initially think this is a standard coin change problem where you can mix different denominations. However, the constraint explicitly states you can only use multiple coins of the same denomination.

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

3. 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).

4. 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

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

5. Incorrect Binary Search Bounds

Using too small an upper bound for binary search. 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.

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

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More