AcWing 214. Devu和鲜花

\(AcWing\) \(214\). \(Devu\)和鲜花

一、题目描述

\(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}\)

输入样例:

3 5
1 3 2

输出样例:

3

二、隔板法复习

1、隔板基本法【每组小球数至少为\(1\)

\(Q\):有 m 个相同的小球,要求分到 n 个盒子里,每组至少小球数为\(1\),问有多少种不同的分法?

\(A\):从m-1个空隙中找出n-1个位置放上隔板,就可以保证最终分成n组,并且,每一组的数量都最少是1个或以上。

其实,有多少种划分方法,就是有多少种不同颜色组合的方案,比如:

如上图,把隔板隔开的不同小球,涂上不同颜色,不就是不同颜色的组合方案了嘛~

隔板法的思路:找空隙(\(m-1\)个空隙),插入隔板(\(n-1\)个隔板,这样才能分成\(n\)),这类问题比较直观:$$\LARGE C_{m-1}^{n-1} $$

2、隔板扩展法【每组小球数可以为\(0\)

有时,有的问题不能直接使用隔板法,比如:
m 个相同的元素,要求分到 n 组中,每组个数可以为\(0\),问有多少种不同的分法?

这个问题对比上面的 隔板法,区别在于没有强调 每组至少元素数为\(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\). 能被整除的数

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\)

这个限制不好加上去~ ,正着想困难,我们倒着想试试:

问题转化为补集:所有方案数-不满足至少一种条件的方案数

(1)、如果我们把不满足条件的去掉,是不是就行了呢?

什么是不满足条件呢?答:如果某个\(x_i>a_i\),就是不满足! 即\(x_i>=a_i+1\)

\(S_i\)代表第\(i\)组不满足的方案个数,即在第\(i\)组中至少取走\(a_i+1\)个的方案个数。

(2)、把这样不满足的都减掉是不是就是答案呢?

:不是!

比如要求第\(1\)组中不能多于\(2\)个,第\(2\)组中不能多于\(3\)个,现在有一组答案\((3,4,2,..)\),表示第一组选择了\(3\)个,第二组中选择了\(4\)个,很明显这个答案对于两个限制都是不能满足的:

  • ① 在检查到第一组时,此答案被去掉一次
  • ② 在检查到第二组时,它又被去掉了一次

但问题是这只是一组数据,现在被去了两次啊!噢,这是 容斥原理 的问题啊~,还得把减两次的加回来一次才对!

\[\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\),代表从第一组里 至少取出\(A_1+1\)朵花,此时还剩\(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\)

#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\)大佬代码,让我们一起批判他吧:

#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;
}
posted @ 2022-06-15 15:54  糖豆爸爸  阅读(68)  评论(0编辑  收藏  举报
Live2D