|
|
|
|
##[$AcWing$ $214$. $Devu$和鲜花 ](https://www.acwing.com/problem/content/description/216/)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
$Devu$ 有 $N$ 个盒子,第 $i$ 个盒子中有 $A_i$ 枝花。
|
|
|
|
|
同一个盒子内的花颜色相同,不同盒子内的花颜色不同。
|
|
|
|
|
$Devu$ 要从这些盒子中选出 $M$ 枝花组成一束,**求共有多少种方案**。
|
|
|
|
|
|
|
|
|
|
**若两束花每种颜色的花的数量都相同,则认为这两束花是相同的方案。**
|
|
|
|
|
|
|
|
|
|
结果需对 $10^9+7$ 取模之后方可输出。
|
|
|
|
|
|
|
|
|
|
**输入格式**
|
|
|
|
|
第一行包含两个整数 $N$ 和 $M$。
|
|
|
|
|
|
|
|
|
|
第二行包含 $N$ 个空格隔开的整数,表示 $A_1,A_2,…,A_N$。
|
|
|
|
|
|
|
|
|
|
**输出格式**
|
|
|
|
|
输出一个整数,表示方案数量对 $10^9+7$ 取模后的结果。
|
|
|
|
|
|
|
|
|
|
**数据范围**
|
|
|
|
|
$1≤N≤20,0≤M≤10^{14},0≤A_i≤10^{12}$
|
|
|
|
|
|
|
|
|
|
**输入样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
3 5
|
|
|
|
|
1 3 2
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
3
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 二、隔板法复习
|
|
|
|
|
|
|
|
|
|
#### 1、隔板基本法【每组小球数至少为$1$】
|
|
|
|
|
$Q$:有 `m` 个相同的小球,要求分到 `n` 个盒子里,<font color='red'><b>每组至少小球数为$1$</b></font>,问有多少种不同的分法?
|
|
|
|
|
|
|
|
|
|
$A$:从`m-1`个空隙中找出`n-1`个位置放上隔板,就可以保证最终分成`n`组,并且,每一组的数量都最少是`1`个或以上。
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/20190423123438869.png'></center>
|
|
|
|
|
|
|
|
|
|
其实,有多少种划分方法,就是有多少种不同颜色组合的方案,比如:
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/20190423123537228.png'></center>
|
|
|
|
|
|
|
|
|
|
如上图,把隔板隔开的不同小球,涂上不同颜色,不就是不同颜色的组合方案了嘛~
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**隔板法的思路**:找空隙(**共$m-1$个空隙**),插入隔板(**$n-1$个隔板,这样才能分成$n$组**),这类问题比较直观:$$\LARGE C_{m-1}^{n-1} $$
|
|
|
|
|
|
|
|
|
|
#### 2、隔板扩展法【每组小球数可以为$0$】
|
|
|
|
|
有时,有的问题不能直接使用隔板法,比如:
|
|
|
|
|
有 `m` 个相同的元素,要求分到 `n` 组中,<font color='red'><b>每组个数可以为$0$</b></font>,问有多少种不同的分法?
|
|
|
|
|
|
|
|
|
|
这个问题对比上面的 **隔板法**,区别在于没有强调 **每组至少元素数为$1$** ,每组是可以为`0`的,这样一来,无法直接使用隔板法。
|
|
|
|
|
|
|
|
|
|
采用一个 **变形**,就可以使用隔板法:
|
|
|
|
|
|
|
|
|
|
原来有`m`个小球,我再多拿`n`个小球,就是一共`m+n`个小球。这些小球,每组先来一个,就保证了每组 **至少** 小球数为$1$,就 **转化为隔板法的基本问题** 了。
|
|
|
|
|
|
|
|
|
|
现在是`n+m`个小球,空隙是 `n+m-1`个。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
由于还是要分成`n`组,所以现在需要在`n+m-1`个空隙中找到`n-1`个位置放入隔板,分法就是$\large \displaystyle C_{m+n-1}^{n-1}$,最后,把每个分组中再取走增加进去的那个小球,这样就和原问题一致了。
|
|
|
|
|
|
|
|
|
|
### 三、本题思路
|
|
|
|
|
|
|
|
|
|
**[容斥原理模板题 $AcWing$ $890$. 能被整除的数 ](https://www.cnblogs.com/littlehb/p/15389237.html)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 1、转化为 隔板扩展法
|
|
|
|
|
先不考虑每个盒子的个数$A_i$限制,**简化问题** 为:从$n$个盒子中每个都任意选出$x_i$个,每个$x_i$**可以为$0$**,可以使用 **隔板扩展法**,答案:
|
|
|
|
|
$$\large \displaystyle C_{m + n − 1}^{n-1}$$
|
|
|
|
|
|
|
|
|
|
> **解读**:盒子数量 $n$,需要$n-1$个隔板。但要求每个盒子中小球数量可以为$0$,所以,先借$n$个小球,最后归还即可。此时,共$m+n$个小球,有 $m+n-1$个空隙,因为需要划分为$n$个盒子,所以,需要找出$n-1$个空隙,即$C_{m+n-1}^{n-1}$
|
|
|
|
|
|
|
|
|
|
#### 2、每组个数限制$a_i$
|
|
|
|
|
但题目并没有那么简单,每个箱子中选择的个数$x_i$,不是随意多少都行的,需要小于 **现存个数** $a_i$。
|
|
|
|
|
|
|
|
|
|
<font color='red'><b>这个限制不好加上去~</b></font> ,正着想困难,我们倒着想试试:
|
|
|
|
|
|
|
|
|
|
<font color='blue' size=4><b>问题转化为补集:所有方案数-不满足至少一种条件的方案数</b></font>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**(1)、如果我们把不满足条件的去掉,是不是就行了呢?**
|
|
|
|
|
|
|
|
|
|
什么是不满足条件呢?答:如果某个$x_i>a_i$,就是不满足! 即$x_i>=a_i+1$
|
|
|
|
|
|
|
|
|
|
<font color='red'><b>令$S_i$代表第$i$组不满足的方案个数,即在第$i$组中至少取走$a_i+1$个的方案个数。</b></font>
|
|
|
|
|
|
|
|
|
|
**(2)、把这样不满足的都减掉是不是就是答案呢?**
|
|
|
|
|
|
|
|
|
|
**答**:<font color='red'><b>不是!</b></font>
|
|
|
|
|
|
|
|
|
|
比如要求第$1$组中不能多于$2$个,第$2$组中不能多于$3$个,现在有一组答案$(3,4,2,..)$,表示第一组选择了$3$个,第二组中选择了$4$个,很明显这个答案对于两个限制都是不能满足的:
|
|
|
|
|
|
|
|
|
|
- ① 在检查到第一组时,此答案被去掉一次
|
|
|
|
|
- ② 在检查到第二组时,它又被去掉了一次
|
|
|
|
|
|
|
|
|
|
但问题是这只是一组数据,现在被去了两次啊!噢,这是 <font color='red'><b>容斥原理</b></font> 的问题啊~,还得把减两次的加回来一次才对!
|
|
|
|
|
|
|
|
|
|
$$\large C_{m+n-1}^{n-1}-|S_1|-|S_2|-...-|S_n|+|S_1\cap S_2|+|S_1 \cap S_3|+...$$
|
|
|
|
|
|
|
|
|
|
> **解读**:模板题中奇数的加,偶数的减,这里怎么是奇数的减,偶数的加呢?
|
|
|
|
|
> 思考一下知道,这应该是题目本来就是要 **去掉不合法** 的情况,去掉嘛,就是减
|
|
|
|
|
> **小结**:受一个条件限制的,需要减去,受两个条件限制的由于已经被单个条件减去了两次,所以需要加上两者的交集,以此类推,就是 **奇数次出现的减,偶数次出现的加**。
|
|
|
|
|
> **总结**:容斥原理可不是一定要奇数加,偶数减,需要具体问题具体分析。究其原因,就是因为前面可能有负号,脱括号后后面的符号就会反转!
|
|
|
|
|
|
|
|
|
|
**(3)、$S_i$怎么求?**
|
|
|
|
|
|
|
|
|
|
比如求$S_1$,代表从第一组里 <font color='red' size=4><b>至少取出$A_1+1$朵花</b></font>,此时还剩$m−(A_1+1)$朵花。
|
|
|
|
|
|
|
|
|
|
则问题转化为选$m−(A_1+1)$只花分$n$组的问题。
|
|
|
|
|
|
|
|
|
|
> **解读**:
|
|
|
|
|
> 至少取出$A_1+1$只花,那就先拿出来放一边。然后考虑剩下的$m-(A_1+1)$只花怎么分的问题。
|
|
|
|
|
> 把这些花直接分$n$组,分组数量可以为$0$,划分完后,再把刚才拿走的那些花还给第$1$组就行了。
|
|
|
|
|
在$m-(A_1+1)$个小球需要划分$n$组,并且每组个数可以为$0$的方案数:
|
|
|
|
|
>$$\large |S_1|=C_{m-(A_1+1)+n-1}^{n-1}$$
|
|
|
|
|
|
|
|
|
|
**(4)、$|S_1 \cap S_2|$怎么求?**
|
|
|
|
|
$|S_1 \cap S_2|$表示从第一组里取出至少$A_1+1$朵花,并且,从第二组里取出至少$A_2+1$朵花。方案数
|
|
|
|
|
$$\large |S_1 \cap S_2|=C_{m-(A_1+1)-(A_2+1)+n-1}^{n-1}$$
|
|
|
|
|
|
|
|
|
|
> **解读**:参考上面的解释内容,先把$A_1+1,A_2+1$拿出来,然后把剩下的花$m-(A_1+1)-(A_2+1)$只花继续划分为$n$组,每组允许为$0$只花,**隔板扩展法**,最后再把$A_1+1,A_2+1$放到第$1,2$组中去。
|
|
|
|
|
|
|
|
|
|
**(5)、计算公式**
|
|
|
|
|
整理一下,得到:
|
|
|
|
|
|
|
|
|
|
$$\large res=C_{m+n-1}^{n-1}-\sum_{i=1}^{n}C_{m+n-1-(A_i+1)}^{n-1}+\sum_{i<j}^{n}C_{m+n-1-(A_i+1)-(A_j+1)}^ {n-1}-...$$
|
|
|
|
|
|
|
|
|
|
**(6)、为什么可以用费马小定理求逆元?**
|
|
|
|
|
$Q$:$(n−1)!$一定不是质数$p$的倍数吗?为什么呢?
|
|
|
|
|
**答**:$n$最大是$20$,$p$是$1e9 + 7$,考虑$n$是从$1$到$20$的乘积,每一个乘数都$p$互质,所以$(n - 1)!$一定与$p$互质,所以一定不是倍数。
|
|
|
|
|
|
|
|
|
|
**(7)、代码实现**
|
|
|
|
|
从$1$枚举到$2^{n-1}$。然后把 **每一个限制条件** 看成一个二进制位,如果是$1$代表 **遵守** ,$0$代表 **不遵守** 这个条件,奇数个就减,偶数个就加。
|
|
|
|
|
|
|
|
|
|
> **解读**:
|
|
|
|
|
> $0000$代表所有限制条件都不遵守,也就是初始值$C_{n+m-1}^{n-1}$
|
|
|
|
|
> $0001$代表第$1$个约束条件遵守,其它约束条件不遵守
|
|
|
|
|
> ...
|
|
|
|
|
> $1111$代表所有限制条件都遵守
|
|
|
|
|
|
|
|
|
|
怎么去算组合数,可以发现虽然$M$非常大,但是$N$很小,所以只需按着定义去算,大概是$O(N)$的复杂度。
|
|
|
|
|
|
|
|
|
|
总的复杂度:$O(2^N∗N)$
|
|
|
|
|
|
|
|
|
|
#### $Code$
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
#define int long long
|
|
|
|
|
#define endl "\n"
|
|
|
|
|
const int N = 20, mod = 1e9 + 7;
|
|
|
|
|
|
|
|
|
|
int A[N];
|
|
|
|
|
int n, m;
|
|
|
|
|
|
|
|
|
|
// 快速幂
|
|
|
|
|
int qmi(int a, int k) {
|
|
|
|
|
int res = 1;
|
|
|
|
|
while (k) {
|
|
|
|
|
if (k & 1) res = res * a % mod;
|
|
|
|
|
a = a * a % mod;
|
|
|
|
|
k >>= 1;
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int C(int a, int b) {
|
|
|
|
|
if (a < b) return 0;
|
|
|
|
|
int up = 1, down = 1;
|
|
|
|
|
for (int i = a; i > a - b; i--) up = i % mod * up % mod;
|
|
|
|
|
for (int i = 1; i <= n - 1; i++) down = i * down % mod; //(n-1)! % mod
|
|
|
|
|
down = qmi(down, mod - 2); // 费马小定理求逆元
|
|
|
|
|
|
|
|
|
|
return up * down % mod; // 费马小定理
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signed main() {
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 0; i < n; i++) cin >> A[i];
|
|
|
|
|
|
|
|
|
|
int res = C(n + m - 1, n - 1);
|
|
|
|
|
for (int i = 1; i < (1 << n); i++) {
|
|
|
|
|
int sum = 0, cnt = 0;
|
|
|
|
|
for (int j = 0; j < n; j++) {
|
|
|
|
|
if (i >> j & 1) {
|
|
|
|
|
sum += A[j] + 1;
|
|
|
|
|
cnt++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (cnt & 1)
|
|
|
|
|
res = (res - C(m + n - 1 - sum, n - 1) + mod) % mod;
|
|
|
|
|
else
|
|
|
|
|
res = (res + C(m + n - 1 - sum, n - 1)) % mod;
|
|
|
|
|
}
|
|
|
|
|
cout << res << endl;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
附上最开始我没有看懂的$yxc$大佬代码,让我们一起批判他吧:
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
#define int long long
|
|
|
|
|
#define endl "\n"
|
|
|
|
|
const int N = 20, mod = 1e9 + 7;
|
|
|
|
|
|
|
|
|
|
int A[N];
|
|
|
|
|
int n, m;
|
|
|
|
|
|
|
|
|
|
// 快速幂
|
|
|
|
|
int qmi(int a, int k) {
|
|
|
|
|
int res = 1;
|
|
|
|
|
while (k) {
|
|
|
|
|
if (k & 1) res = res * a % mod;
|
|
|
|
|
a = a * a % mod;
|
|
|
|
|
k >>= 1;
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int C(int a, int b) {
|
|
|
|
|
if (a < b) return 0;
|
|
|
|
|
int up = 1, down = 1;
|
|
|
|
|
for (int i = a; i > a - b; i--) up = i % mod * up % mod;
|
|
|
|
|
for (int i = 1; i <= n - 1; i++) down = i * down % mod; //(n-1)! % mod
|
|
|
|
|
down = qmi(down, mod - 2); // 费马小定理求逆元
|
|
|
|
|
|
|
|
|
|
return up * down % mod; // 费马小定理
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signed main() {
|
|
|
|
|
cin >> n >> m;
|
|
|
|
|
for (int i = 0; i < n; i++) cin >> A[i]; // 第i个盒子中有A[i]枝花,限制条件
|
|
|
|
|
|
|
|
|
|
// yxc这里写的代码太随意了,把我直接干蒙圈了!
|
|
|
|
|
// 根据推导的式子,这里需要一个全部方案数=C(n + m - 1, n - 1)
|
|
|
|
|
// 也就是说 res的初始值就是上面的全部方案数。
|
|
|
|
|
// 可是,yxc大佬的大脑与正常人不一样,他居然没有给初始值,直接把初始值也写到下面的容斥原理代码中!!!
|
|
|
|
|
// 也就是所有限制条件全部不采用,也就是全部不受限制!也就是全部方案数!!!
|
|
|
|
|
int res = 0;
|
|
|
|
|
for (int i = 0; i < 1 << n; i++) { // 容斥原理的项数,0000 代表所有限制条件都不遵守,0001代表第1个限制条件遵守,其它3个不遵守
|
|
|
|
|
int sum = 0, cnt = 0; // 奇数个限制条件,需要减;偶数个限制条件,需要加。现在这种限制条件组合状态,是奇数个限制,还是偶数个限制?
|
|
|
|
|
for (int j = 0; j < n; j++) // 枚举状态的每一位
|
|
|
|
|
if (i >> j & 1) { // 如果此位是1
|
|
|
|
|
sum += A[j] + 1; // 拼公式
|
|
|
|
|
cnt++; // 限制条件个数,奇数个减,偶数个加
|
|
|
|
|
}
|
|
|
|
|
if (cnt & 1)
|
|
|
|
|
res = (res - C(m + n - 1 - sum, n - 1) + mod) % mod;
|
|
|
|
|
else
|
|
|
|
|
res = (res + C(m + n - 1 - sum, n - 1)) % mod;
|
|
|
|
|
}
|
|
|
|
|
cout << (res + mod) % mod << endl;
|
|
|
|
|
}
|
|
|
|
|
```
|