Kth Smallest Amount With Single Denomination Combination
HardPrompt
You are given an integer array coins representing coins of different denominations and an integer k.
You have an infinite number of coins of each denomination. However, you are not allowed to combine coins of different denominations.
Return the kth smallest amount that can be made using these coins.
Example 1:
Input: coins = [3,6,9], k = 3
Output: 9
Explanation: The given coins can make the following amounts:
Coin 3 produces multiples of 3: 3, 6, 9, 12, 15, etc.
Coin 6 produces multiples of 6: 6, 12, 18, 24, etc.
Coin 9 produces multiples of 9: 9, 18, 27, 36, etc.
All of the coins combined produce: 3, 6, 9, 12, 15, etc.
Example 2:
Input: coins = [5,2], k = 7
Output: 12
Explanation: The given coins can make the following amounts:
Coin 5 produces multiples of 5: 5, 10, 15, 20, etc.
Coin 2 produces multiples of 2: 2, 4, 6, 8, 10, 12, etc.
All of the coins combined produce: 2, 4, 5, 6, 8, 10, 12, 14, 15, etc.
Constraints:
1 <= coins.length <= 151 <= coins[i] <= 251 <= k <= 2 * 109coinscontains pairwise distinct integers.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach simulates the process of generating amounts in increasing order. We use a min-heap (Priority Queue in Java) to efficiently keep track of and retrieve the next smallest amount that can be formed from the multiples of all coins. This can be visualized as merging N sorted lists, where N is the number of coins.
Algorithm
- Preprocessing (Optional but Recommended): Filter the
coinsarray to remove any coinc1that is a multiple of another coinc2in the array. This is because any amount that is a multiple ofc1is also a multiple ofc2, soc1is redundant. - Initialization: Create a min-priority queue to store arrays of
[amount, original_coin]. For each non-redundant coin, add[coin, coin]to the priority queue. - Iteration: Maintain a count of unique amounts found and the value of the last unique amount processed (
last_amount). - Main Loop: Repeatedly extract the element with the smallest amount from the priority queue.
- If the extracted
current_amountis the same aslast_amount, it's a duplicate (e.g., 6 from coin 2 and 6 from coin 3). We ignore it but still process its next multiple. - If
current_amountis greater thanlast_amount, it's a new unique amount. Increment thecountand updatelast_amount. - If
countreachesk, we have found our answer. - Add the next multiple,
[current_amount + original_coin, original_coin], back into the priority queue.
- If the extracted
Walkthrough
The core idea is to treat the multiples of each coin as a sorted stream of numbers. We want to find the k-th smallest element from the union of these streams. A min-heap is the perfect data structure for this 'merging' operation.
We initialize the heap with the first multiple of each coin (the coin values themselves). Then, we repeatedly extract the minimum element from the heap. This element is the next smallest amount in the combined sequence. To handle cases where the same amount can be generated by different coins (e.g., 12 is a multiple of 3, 6), we only increment our count for new, unique amounts. After processing an amount m generated by a coin c, we add the next multiple, m + c, into the heap. We continue this process until we have found k unique amounts.
import java.util.*; class Solution { public long kthSmallestAmount(int[] coins, int k) { // This approach is too slow for the given constraints but illustrates the idea. // It would work for smaller k. PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(a -> a[0])); // Pre-filtering coins can be an optimization but doesn't change the main complexity issue with large k. // For simplicity, we'll use all coins. for (int coin : coins) { pq.offer(new long[]{coin, coin}); } long lastAmount = -1; int count = 0; while (!pq.isEmpty()) { long[] current = pq.poll(); long currentAmount = current[0]; long originalCoin = current[1]; if (currentAmount > lastAmount) { count++; lastAmount = currentAmount; if (count == k) { return currentAmount; } } // Add the next multiple, checking for overflow if (Long.MAX_VALUE - originalCoin >= currentAmount) { pq.offer(new long[]{currentAmount + originalCoin, originalCoin}); } } return -1; // Should not be reached }}Complexity
Time
O(k * N * log N). In the worst case, we might have to perform close to `k * N` poll operations from the heap due to duplicate amounts. Each heap operation takes `O(log N)` time. This is too slow for the given constraints on `k`.
Space
O(N), where N is the number of coins. The priority queue stores at most N elements at any time (one for each coin's stream of multiples).
Trade-offs
Pros
Conceptually simpler and more intuitive than the optimal mathematical approach.
Relatively easy to implement if familiar with priority queues.
Cons
The time complexity is proportional to
k, which is too slow for the given constraints wherekcan be up to2 * 10^9.This approach will likely result in a 'Time Limit Exceeded' error on platforms with strict time limits.
This highly efficient approach leverages the monotonic property of the problem: if an amount M has c valid amounts less than or equal to it, any amount M' > M will have at least c amounts. This allows us to binary search for the k-th smallest amount. The main challenge is to create a function that, for any given amount X, can count how many valid amounts are less than or equal to X. This counting problem is solved using the Principle of Inclusion-Exclusion (PIE).
Algorithm
- Preprocessing: As an optimization, filter the
coinsarray. A coinc1is redundant if it's a multiple of another coinc2. Removing these can reducenand speed up the PIE calculation. - Binary Search Setup: Define a search range for the answer. A safe range is
[1, k * min_coin], as thek-th amount cannot be larger than thek-th multiple of the smallest coin. - Binary Search Loop:
- Pick a
midvalue in the current search range. - Calculate
count = countLe(mid), which is the number of unique amounts less than or equal tomid. - If
count >= k, it meansmidis a potential answer, and the actual answer could bemidor something smaller. So, we recordmidand shrink the search space to the lower half:high = mid - 1. - If
count < k,midis too small. The answer must be larger. We search in the upper half:low = mid + 1.
- Pick a
countLe(M)Function (PIE):- This function calculates
|U S_i|whereS_iare multiples ofcoins[i]up toM. - Iterate through all
2^n - 1non-empty subsets of the (filtered) coins. - For each subset, calculate the Least Common Multiple (LCM) of its elements. Be careful to handle potential
longoverflows during LCM calculation. If the LCM exceedsM, its contribution is zero. - If the subset size is odd, add
M / lcmto the total count. - If the subset size is even, subtract
M / lcmfrom the total count.
- This function calculates
- Return Value: The binary search terminates when
low > high, and the last recorded potential answer is thek-th smallest amount.
Walkthrough
The search space for the answer can be huge, but we can efficiently find the k-th amount by binary searching on the value of the amount itself. For any candidate amount M, we need to determine its rank, i.e., how many unique amounts are less than or equal to M. Let's call this function countLe(M).
If countLe(M) < k, M is too small, and we must search for a larger amount. If countLe(M) >= k, M could be our answer, or the answer could be even smaller. This is the standard binary search pattern for finding the minimum value that satisfies a condition.
The countLe(M) function is the heart of this solution. The total number of amounts less than or equal to M is the size of the union of the sets of multiples for each coin. This is calculated using the Principle of Inclusion-Exclusion.
countLe(M) = Σ floor(M/c_i) - Σ floor(M/lcm(c_i, c_j)) + Σ floor(M/lcm(c_i, c_j, c_k)) - ...
We can implement this by iterating through all 2^n - 1 non-empty subsets of the coins. For each subset, we compute the LCM of the coins within it and add or subtract M / LCM based on the subset's size.
import java.util.*; class Solution { public long kthSmallestAmount(int[] coins, int k) { int n = coins.length; List<Integer> uniqueCoins = new ArrayList<>(); Arrays.sort(coins); for (int i = 0; i < n; i++) { boolean isRedundant = false; for (int j = 0; j < i; j++) { if (coins[i] % coins[j] == 0) { isRedundant = true; break; } } if (!isRedundant) { uniqueCoins.add(coins[i]); } } int[] filteredCoins = uniqueCoins.stream().mapToInt(i -> i).toArray(); n = filteredCoins.length; long low = 1; long high = (long) k * filteredCoins[0]; long ans = high; while (low <= high) { long mid = low + (high - low) / 2; if (countLe(mid, filteredCoins) >= k) { ans = mid; high = mid - 1; } else { low = mid + 1; } } return ans; } private long countLe(long m, int[] coins) { int n = coins.length; long count = 0; for (int i = 1; i < (1 << n); i++) { long currentLcm = 1; int subsetSize = 0; for (int j = 0; j < n; j++) { if ((i & (1 << j)) != 0) { subsetSize++; currentLcm = lcm(currentLcm, coins[j]); if (currentLcm > m) { // Optimization currentLcm = m + 1; break; } } } if (currentLcm <= m) { if (subsetSize % 2 == 1) { count += m / currentLcm; } else { count -= m / currentLcm; } } } return count; } private long gcd(long a, long b) { return b == 0 ? a : gcd(b, a % b); } private long lcm(long a, long b) { if (a == 0 || b == 0) return 0; // Check for overflow before multiplication if (a > Long.MAX_VALUE / b) return Long.MAX_VALUE; long res = a * b / gcd(a, b); return res; }}Complexity
Time
O(N^2 + log(R) * 2^N * log(M)). `N` is the number of coins (`<=15`), `R` is the binary search range (up to `k * C_min`), and `M` is a value in that range. `N^2` is for preprocessing. `log(R)` is for binary search iterations. `2^N` is for iterating through subsets for PIE. `log(M)` is for the GCD calculation within the LCM. This is efficient enough to pass.
Space
O(N), where N is the number of coins. This space is used to store the filtered coins and for the recursion stack if PIE is implemented recursively. The iterative implementation also has minimal space overhead.
Trade-offs
Pros
Extremely efficient and scalable for large values of
k.The time complexity does not depend on
k, but rather on the number of coinsN.Optimal solution for the given constraints.
Cons
More complex to understand and implement correctly compared to a direct simulation.
Requires careful handling of the Principle of Inclusion-Exclusion logic.
LCM calculations for many numbers can lead to very large values, requiring
longand checks for overflow.
Solutions
Solution
class Solution {private int[] coins;private int k;public long findKthSmallest(int[] coins, int k) { this.coins = coins; this.k = k; long l = 1, r = (long)1 e11; while (l < r) { long mid = (l + r) >> 1; if (check(mid)) { r = mid; } else { l = mid + 1; } } return l; }private boolean check(long mx) { long cnt = 0; int n = coins.length; for (int i = 1; i < 1 << n; ++i) { long v = 1; for (int j = 0; j < n; ++j) { if ((i >> j & 1) == 1) { v = lcm(v, coins[j]); if (v > mx) { break; } } } int m = Integer.bitCount(i); if (m % 2 == 1) { cnt += mx / v; } else { cnt -= mx / v; } } return cnt >= k; }private long lcm(long a, long b) { return a * b / gcd(a, b); }private long gcd(long a, long b) { return b == 0 ? a : gcd(b, a % b); }}class Solution {public: long long findKthSmallest(vector<int> &coins, int k) { using ll = long long; ll l = 1, r = 1e11; int n = coins.size(); auto check = [&](ll mx) { ll cnt = 0; for (int i = 1; i < 1 << n; ++i) { ll v = 1; for (int j = 0; j < n; ++j) { if (i >> j & 1) { v = lcm(v, coins[j]); if (v > mx) { break; } } } int m = __builtin_popcount(i); if (m & 1) { cnt += mx / v; } else { cnt -= mx / v; } } return cnt >= k; }; while (l < r) { ll mid = (l + r) >> 1; if (check(mid)) { r = mid; } else { l = mid + 1; } } return l; }};class Solution: def findKthSmallest(self, coins: List[int], k: int) -> int: def check(mx: int) -> bool: cnt = 0 for i in range(1, 1 << len(coins)): v = 1 for j, x in enumerate(coins): if i >> j & 1: v = lcm(v, x) if v > mx: break m = i . bit_count() if m & 1: cnt += mx // v else: cnt -= mx // v return cnt >= k return bisect_left(range(10 ** 11), True, key=check)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.