Leetcode 第 393 场周赛 T3 - 单面值组合的第 K 小金额

100267. 单面值组合的第 K 小金额

[困难]

给你一个整数数组 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 包含两两不同的整数。

容斥原理 + 二分查找

原题解 - 本篇题解是对原题解的个人理解和补充

容斥原理的最终普适模板题,相似的题包括:

  1. 两数容斥 - 878. 第 N 个神奇数字
  2. 三数容斥 - 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| AB=A+BAB
以及三个集合的容斥:
∣ 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| ABC=A+B+CABBCCA+ABC

发现规律了吗?集合交集的个数是其符号的决定因素,比如两个集合的交集则是减去重复,三个集合的交集则是补全缺漏,所以多个集合的容斥公式其实就是每个集合的组合模式的和,其中每个项的正负由参与该项的集合的个数决定,奇数个为正,偶数个为负。

如何遍历一个集合所有元素组合可能形成的子集(包括空集)?我们可以使用一个整型的二进制位来代表集合中的第 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 45)而后续的几个 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;
    }
};
  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值