You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 214. Devu和鲜花

一、题目描述

DevuN 个盒子,第 i 个盒子中有 A_i 枝花。 同一个盒子内的花颜色相同,不同盒子内的花颜色不同。 Devu 要从这些盒子中选出 M 枝花组成一束,求共有多少种方案

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

结果需对 10^9+7 取模之后方可输出。

输入格式 第一行包含两个整数 NM

第二行包含 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放到第12组中去。

(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:(n1)!一定不是质数p的倍数吗?为什么呢? n最大是20p1e9 + 7,考虑n是从120的乘积,每一个乘数都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^NN)

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;
}