Devu和鲜花

Devu和鲜花

Devu 有 N 个盒子,第 i 个盒子中有 Ai 枝花。

同一个盒子内的花颜色相同,不同盒子内的花颜色不同。

Devu 要从这些盒子中选出 M 枝花组成一束,求共有多少种方案。

若两束花每种颜色的花的数量都相同,则认为这两束花是相同的方案。

结果需对 109+7 取模之后方可输出。

输入格式

第一行包含两个整数 NM

第二行包含 N 个空格隔开的整数,表示 A1,A2,,AN

输出格式

输出一个整数,表示方案数量对 109+7 取模后的结果。

数据范围

1N20,
0M1014,
0Ai1012

输入样例:

3 5
1 3 2

输出样例:

3

 

解题思路

  这题是用容斥原理来做的。

  先考虑每个盒子中花的数量为无限个,假设xi表示在第i个盒子中选择花的数量,由于要从n个盒子中选择m枝花,因此有x1+x2++xn=m,其中xi0。最终选法的个数就是这个方程的非负整数解的个数。这个可以用隔板法来求,由于在隔板法中要求变量的值为正整数,因此还需要对这个方程做一个映射。设yi=xi+1,因此有yi1,等式就变成了y1+y2++yn=m+n,因此正整数解的方案数就是Cm+n1n1

  在原问题中每个盒子选择花的数量是有限制的,即要求xiai,而上面的做法是没有限制的情况(即总方案数),为了通过没有限制的情况得到有限制的情况,这里就可以用容斥原理了。由于有n个条件要同时满足,我们可以求补集,即求至少有一个条件不满足的方案数量,然后用总的方案数量Cm+n1n1减去至少有一个条件不满足的方案数量,得到的结果就是同时满足n个条件的数量。

  如果第i个条件不满足,那么就是xiai+1。设集合Si表示不满足第i个条件的方案,因此同时满足n个条件的方案数就是Cm+n1n1|i=1nSi|

  先分析一下|S1|如何求。第1个盒子至少要拿ai+1枝花,那么我们先从第1个盒子取出a1+1枝花,此时问题就变成了从n个盒子中选择m(a1+1)枝花。为什么还要从第1个盒子取花?这是因为第1个盒子至少要取a1+1枝花,因此第1个盒子还可以选,先取出a1+1枝花,后面不管从第1个盒子取多少花最终都至少从第1个盒子取出超过a1枝花。因此有|S1|=Cm(a1+1)+n1n1

  因此|Si|=Cm(ai+1)+n1n1,那么|SiSj|呢?同理先从第i个盒子取出ai+1枝花,再从第j个盒子取出aj+1枝花,问题就变成了从n个盒子中选择m(ai+1)(aj+1)枝花,方案数就是|SiSj|=Cm(ai+1)(aj+1)+n1n1 。以此类推来求|SiSjSk|的方案数。

  因此可以通过二进制枚举来实现求容斥原理的过程。

  其中由于n最大只有20,因此公式中的组合数Cm+n1n1直接按照定义(m+n1)(m+n2)(m+1)(n1)!来算就可以了。除法用乘法逆元,由于取模的数是质数,n最大取到20,因此(n1)!必然与109+7互质,因此可以用费马小定理来求逆元。注意到公式中的组合数都是取出n1个数,即分母都是(n1)!,因此可以先预处理出(n1)!的逆元,这样每次求组合数就不需要重复计算了。

  AC代码如下,时间复杂度为O(n2n)

复制代码
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 typedef long long LL;
 5 
 6 const int N = 30, mod = 1e9 + 7;
 7 
 8 LL p[N];
 9 LL down = 1;    // (n-1)!的逆元
10 
11 int qmi(int a, int k) {
12     int ret = 1;
13     while (k) {
14         if (k & 1) ret = 1ll * ret * a % mod;
15         a = 1ll * a * a % mod;
16         k >>= 1;
17     }
18     return ret;
19 }
20 
21 int C(LL a, LL b) { // 从a个里面选b个
22     if (a < b) return 0;
23     int up = 1; // 分子
24     for (LL i = a; i > a - b; i--) {
25         up = i % mod * up % mod;
26     }
27     return up * down % mod; // 分子乘以分母的逆元
28 }
29 
30 int main() {
31     LL n, m;
32     scanf("%lld %lld", &n, &m);
33     for (int i = 0; i < n; i++) {
34         scanf("%lld", p + i);
35     }
36     for (int i = 1; i < n; i++) {
37         down = down * i % mod;
38     }
39     down = qmi(down, mod - 2);  // 求分母的逆元
40     int ret = C(m + n - 1, n - 1);
41     for (int i = 1; i < 1 << n; i++) {
42         LL a = m + n - 1, b = n - 1, cnt = 0;
43         for (int j = 0; j < n; j++) {
44             if (i >> j & 1) a -= p[j] + 1, cnt ^= 1;
45         }
46         if (cnt) ret = (ret - C(a, b)) % mod;
47         else ret = (ret + C(a, b)) % mod;
48     }
49     printf("%d", (ret + mod) % mod);
50     
51     return 0;
52 }
复制代码

 

参考资料

  AcWing 214. Devu和鲜花(算法提高课):https://www.acwing.com/video/739/

posted @   onlyblues  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2022-02-22 数星星
Web Analytics
点击右上角即可分享
微信分享提示