[困难]
给你一个整数数组 coins
表示不同面额的硬币,另给你一个整数 k
。
你有无限量的每种面额的硬币。但是,你 不能 组合使用不同面额的硬币。
返回使用这些硬币能制造的 第 kth
小 金额。
示例 1:
输入: coins = [3,6,9]
, k = 3
输出: 9
解释: 给定的硬币可以制造以下金额:
3元硬币产生3的倍数:3, 6, 9, 12, 15等。
6元硬币产生6的倍数:6, 12, 18, 24等。
9元硬币产生9的倍数:9, 18, 27, 36等。
所有硬币合起来可以产生:3, 6, 9, 12, 15等。
示例 2:
输入: coins = [5,2]
, k = 7
输出: 12
解释: 给定的硬币可以制造以下金额:
5元硬币产生5的倍数:5, 10, 15, 20等。
2元硬币产生2的倍数:2, 4, 6, 8, 10, 12等。
所有硬币合起来可以产生:2, 4, 5, 6, 8, 10, 12, 14, 15等。
提示:
1 <= coins.length <= 15
1 <= coins[i] <= 25
1 <= k <= 2 * 10^9
coins
包含两两不同的整数。
容斥原理 + 二分查找
原题解 - 本篇题解是对原题解的个人理解和补充
容斥原理的最终普适模板题,相似的题包括:
- 两数容斥 - 878. 第 N 个神奇数字
- 三数容斥 - 1201. 丑数 III(可以看我写的另外一篇记录)
请务必先熟悉以上两题的容斥原理解法,理解容斥原理公式的组成之后再来本题。同时熟记最大公约数和最小公倍数的板子。
为什么这题是容斥原理的模板题呢,其实我们把题目换一个描述方式即可:称一个数是好的,若它能被 coins
里至少一种元素整除。求第
k
k
k 个好数。给定几个基数,求这几个基数各自的倍数所构成的集合中,排序之后第
k
k
k 个不重复的数是多少,所以这个集合中的任何一个数,之所以能存在于这个集合,肯定是它能被基数中的其中一个基数整除,也就是它的倍数。那么现在我们就将题目转化为了之前的容斥原理题目,现在要求各个基数的不同组合模式中最大公约数(GCD)和最小公倍数(LCM)。
对于两个集合和三个集合的容斥,我们还可以手推公式,但是对于多个集合的容斥该怎么办呢?(在本题中一个集合指的是一个基数的所有倍数)
可以知道两个集合的容斥:
∣
A
∪
B
∣
=
∣
A
∣
+
∣
B
∣
−
∣
A
∩
B
∣
|A\cup B| = |A|+|B|-|A\cap B|
∣A∪B∣=∣A∣+∣B∣−∣A∩B∣
以及三个集合的容斥:
∣
A
∪
B
∪
C
∣
=
∣
A
∣
+
∣
B
∣
+
∣
C
∣
−
∣
A
∩
B
∣
−
∣
B
∩
C
∣
−
∣
C
∩
A
∣
+
∣
A
∩
B
∩
C
∣
|A\cup B\cup C| = |A|+|B|+|C|-|A\cap B|-|B\cap C|-|C\cap A|+|A\cap B\cap C|
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣B∩C∣−∣C∩A∣+∣A∩B∩C∣
发现规律了吗?集合交集的个数是其符号的决定因素,比如两个集合的交集则是减去重复,三个集合的交集则是补全缺漏,所以多个集合的容斥公式其实就是每个集合的组合模式的和,其中每个项的正负由参与该项的集合的个数决定,奇数个为正,偶数个为负。
如何遍历一个集合所有元素组合可能形成的子集(包括空集)?我们可以使用一个整型的二进制位来代表集合中的第 i i i 个元素是否参与到当前子集组合,如果 x i = 0 x_i = 0 xi=0 则表示不参与, x i = 1 x_i=1 xi=1 则表示参与,一共有 n n n 个二进制位,所以可构造的子集数量是 2 n 2^n 2n 。
现在我们可以通过计算区间
[
1
,
x
]
[1,\ x]
[1, x] 中能被基数集合中至少一个数整除的数的数量了,我们可以通过二分查找的方式逐步缩小区间右端点
x
x
x 的取值范围,直到我们搜索完整个取值空间,就可以唯一确定
x
x
x 的值。注意此处的二分查找不是找到目标值即刻退出,而是应该将整个搜索空间排除完(也就是当
l
<
r
l < r
l<r 时持续搜索),这是因为并不是区间端点往右增长
+
1
+1
+1 则区间内合法的数(至少被基数集合中的一个数整除)就一定会增长,所以二分查找的时候可能会遇到多个重复的值,在这种情况下,只有端点第一个变化的值才是有效的值,比如 [4,5,5,5,6]
这种搜索情况,第一个 5 代表当区间端点
x
x
x 选择该处时,第一次出现了多一个合法的数(由
4
→
5
4 \rightarrow 5
4→5)而后续的几个 5 代表尽管区间在扩大,但是没有新的合法的数出现,此时的区间端点
x
x
x 是非法的。
class Solution {
typedef long long ll;
private:
ll gcd(ll x, ll y) {
if (x % y == 0) return y;
if (y % x == 0) return x;
ll m;
while (x % y) {
m = x % y;
x = y;
y = m;
}
return m;
}
ll lcm(ll x, ll y) {
return x / gcd(x, y) * y;
}
public:
ll findKthSmallest(vector<int>& coins, int k) {
int n = coins.size();
std::function<bool(const ll&)> check = [&](const ll& x) {
ll cnt = 0;
for (int i = 1; i < (1 << n); i++) {
ll sign = -1; // 减去重复或者添加新的
ll lcmm = 1; // 计算所有值最小公倍数
for (int j = 0; j < n; j++) {
if ((i >> j & 1) == 0) continue;
sign = -sign;
lcmm = lcm(lcmm, coins[j]);
if (lcmm > x) break; // 最小公倍数超过基数就没必要继续算了,x / lcmm = 0
}
cnt += sign * (x / lcmm);
}
return cnt >= k;
};
ll l = 1, r = LLONG_MAX, m;
while (l < r) {
m = l + ((r - l) >> 1);
if (check(m)) r = m;
else l = m + 1;
}
return r;
}
};