Files
python/TangDou/AcWing_TiGao/T5/RongChi/214.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

261 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

##[$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`个。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312060828700.png)
由于还是要分成`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;
}
```