ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

AcWing 214. Devu和鲜花

2022-06-15 16:02:11  阅读:145  来源: 互联网

标签:隔板 214 int res LL 小球 Devu AcWing mod


题目传送门

\(Devu\) 有 \(N\) 个盒子,第 \(i\) 个盒子中有 \(A_i\) 枝花。
同一个盒子内的花颜色相同,不同盒子内的花颜色不同。
\(Devu\) 要从这些盒子中选出 \(M\) 枝花组成一束,求共有多少种方案
若两束花每种颜色的花的数量都相同,则认为这两束花是相同的方案。
结果需对 10^9+7 取模之后方可输出。

一、隔板法复习

隔板法

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

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

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

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

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

隔板法扩展

有时,有的问题不能直接使用隔板法,比如:
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}\),最后,把每个分组中再取走增加进去的那个小球,这样,就和原问题一致了。

二、本题思路

1、转化为隔板法扩展

题目要求,每个盒子有\(a_i\)个花,想要从\(n\)个盒子中每个都任意选出\(x_i\)​个,每个\(x_i\)可以为\(0\),需要使用隔板法扩展,答案:\(\LARGE \displaystyle C_{m + n − 1}^{n-1}\)


2、每组个数限制

但是题目并没有那么简单,我们 仍需满足$$\large ∀x_i ≤a_i$$

这个限制不好加上去~ ,正着想困难,我们试试倒着想(遇事不决,小学数学!逆向思维法)

如果每一个盒子都可以随意挑,没有任何限制 ,那么答案是

\[\large \displaystyle C_{m+n-1}^{n-1} \]

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

什么是不满足条件呢?答:如果某个\(\large x_i>a_i\),就是不满足! 令\(S_i\)代表被\(i\)组不满足的方案个数。

\(Q\):我们把这样不满足的都减掉是不是就是答案呢?不是的!

\(A\):为什么不是啊?比如要求第\(1\)组中不能多于\(2\)个,第\(2\)组中不能多于\(3\)个,
我们就可以有一组答案\((3,4)\),表示第一组选择了\(3\)个,第二组中选择了\(4\)个,这样的数据,很显然是不可以成为答案的,那么,在检查到第一组时,它被去掉一次,在检查到第二组时,它又被去掉了一次,但问题是这只是一组数据,被去了两次啊!噢,这是 容斥原理的问题啊~,还得把减两次的加回来一次才对!

写成数学表达式就是:

\[\large C_{m+n-1}^{n-1}-|S_1 \cup S_2 \cup ...\cup S_n| \]

后面这一坨,我学过,这是容斥原理:

\[\large C_{m+n-1}^{n-1}-|S_1|-|S_2|-...-|S_n|+|S_1\cap S_2|+|S_1 \cap S_3|+... \]

也就是减去一个的,加上两个的,减去三个的,加上四个的...
抽象一下就是:减去奇数个条件限制的,加上偶数个条件限制的

柿子推完,不代表题目已经完了,这里我们还要思考一个问题:\(\LARGE S_i\) 怎么求?

首先,我们在明确一下\(\LARGE S_i\)的定义:即,在第\(i\)个物品中取出至少\(a_i + 1\)个物品

以\(S_i\) 举例子,我们总共有\(m + n\)个,现在取走\(a_i + 1\),剩下\(m+n−(a_i+1)\)
个,第\(i\)组的个数现在是\(a_i+1\)个,我们视为是\(>1\)的个数;同时,因为不能歧视第\(i\)组,再分配小球时,第\(i\)组可以分,也可以不分,说白了,就是和其它组没有区别(有点像高考前OI获奖,总成绩先加了\(20\)分,^^,狗头保命~),那么还是要划分为\(n\)组,还是要加入\(n-1\)个隔板,那么就应该是$$ \LARGE |S_i|= C{m+n-1-(a_i+1)}^{n-1}$$

如果是两个物品都大于等于\(a_i+1\)和\(a_j+1\)呢?
思路是一样的,总共有\(m+n\)个,先给第\(i\)组拿走\(a_i+1\)个,再给第\(j\)组拿走\(a_{j}+1\)个,再一视同仁,划分\(n\)组,需要\(n-1\)个隔板,就是:

\[\LARGE |S_i \cap S_j|=C_{m+n-1-(a_i+1)-(a_{j}+1)}^{n-1} \]


3、每个物品的枚举方法

那么考虑每个物品的枚举方法,因为物品数,很小,我们可以用二进制枚举,枚举的方案数为\(2^n\),算组合数我们用最朴素的方式,时间复杂度大概是:\(O(2n ∗n)\)

这里特殊需要说明一点,就是边界情况:
\(Q\):如果\(\large S_1,S_2,...S_n\)我们一个都不满足,是什么意思呢?
\(A\):就是第\(i\)个盒子中,取几个花不受限制,随意就可好~,全都不满足,就是啥限制没有,全部放开,不就是原来的$\LARGE \displaystyle C_{m-1}^{n-1} $吗?这一点,在后面的代码实现中会得到体现,我们可以枚举容斥原理公式中的每一项,同时,添加上所有限制不选中的情况,就是原来的答案:

\[\LARGE C_{m+n-1}^{n-1}-|S_1|-|S_2|-...-|S_n|+|S_1\cap S_2|+|S_1 \cap S_3|+... \]


4、组合数计算

本题的\(M=10^{14}\)非常大,但是我们的$N \leq 20 \(,所以只需按着定义去算,大概是\)N\(的复杂度。所以总的复杂度就是\)O(2^N∗N)$。本题计算次数约等于\(2000w\)。

\[\large C(a,b)=\frac{a!}{(a-b)!*b!}=\frac{a*(a-1)*(a-2)*...(a-b+1)}{b*(b-1)*(b-2)*...*1} \]

费马小定理(如果\(p\)是一个质数(本题中\(p=1e9+7\),妥妥的质数),而整数\(a\)不是\(p\)的倍数,则有\(a^{p−1}≡1(mod \ p))\)

求\(\large \displaystyle \frac{1}{(n−1)!}\)
\(\large \displaystyle a=(n-1)!\)
\(a_{p-1}=1 (mod \ p)\)
那么\(a_{p-2}=a^{-1} (mod \ p)\)
也就是说\(a\)的逆元为\(a^{p-2}\)。

\(Q1\):费马小定理可以使用的条件是 \(p\)是质数,并且\(a\)不能是\(p\)的倍数,现在\(p\)肯定是质数,但\((n-1)!\)一定不是\(p\)的倍数吗?为什么呢?
\(A\): 因为\(p\)很大,而\(n\)很小, \((n-1)!\)最大就是\(1*2*3*...*20\),这里面是不可能出现\(p\)这个因子的,所以,\((n_1)!\)必然不是\(p\)的倍数,可以使用费马小定理。

\(Q2\):在代码实现中,一边计算阶乘,一边进行了取模,是符合什么样的原理?
\(A\): 阶乘可以视为乘法,一般做乘法,一般取模是没有问题的。


三、原始版本代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20, mod = 1e9 + 7;

LL A[N]; // 10^12,好变态的大数,爆int
LL n, m;

//快速幂
int qmi(int a, int k) {
    int res = 1;
    while (k) {
        if (k & 1) res = (LL)res * a % mod;
        a = (LL)a * a % mod;
        k >>= 1;
    }
    return res;
}

//原始版本代码,会TLE最后一个测试点,需要优化一下
int C(LL a, LL b) {
    if (a < b) return 0;
    LL up = 1, down = 1;
    for (LL i = a; i > a - b; i--) up = i % mod * up % mod; //随时小心爆long long
    for (int i = 1; i <= n - 1; i++) down = i * down % mod; //(n-1)! % mod
    down = qmi(down, mod - 2);                              //费马小定理求逆元

    return up * down % mod; // 费马小定理
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> A[i]; //第i个盒子中有A[i]枝花,限制条件

    int res = 0;
    for (int i = 0; i < 1 << n; i++) { //容斥原理的项数,0000 代表一个限制条件都没有, 0001代表第1个限制条件生效,0011,代码第1,2个限制条件生效
        LL a = m + n - 1, b = n - 1;   //上限是m+n-1,下限不一样
        int sign = 1;                  //奇数个限制条件,需要减;偶数个限制条件,需要加。现在这种限制条件组合状态,是奇数个限制,还是偶数个限制?
        for (int j = 0; j < n; j++)    //枚举状态的每一位
            if (i >> j & 1) {          //如果此位是1
                sign *= -1;            //符号位变号,从1变为-1,或者,从-1变为1; 这个用法挺牛X的,漂亮的实现了奇数次负号,偶数次正号的维护
                a -= A[j] + 1;         //拼公式
            }
        res = (res + C(a, b) * sign) % mod;
    }
    cout << (res + mod) % mod << endl;
    return 0;
}

四、优化版本

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20, mod = 1e9 + 7;

LL A[N]; // 10^12,好变态的大数,爆int
LL n, m;

//快速幂
int qmi(int a, int k) {
    int res = 1;
    while (k) {
        if (k & 1) res = (LL)res * a % mod;
        a = (LL)a * a % mod;
        k >>= 1;
    }
    return res;
}

//优化版本代码,因为本题的分母是(n-1)!,每次计算都是一样的,没有必要重复计算
LL down = 1;
int C(LL a, LL b) {
    if (a < b) return 0;
    LL up = 1;
    for (LL i = a; i > a - b; i--) up = i % mod * up % mod; //随时小心爆long long

    return up * down % mod; // 费马小定理
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> A[i]; //第i个盒子中有A[i]枝花,限制条件

    for (int i = 1; i <= n - 1; i++) down = i * down % mod; //(n-1)! % mod
    down = qmi(down, mod - 2);                              //费马小定理求逆元

    int res = 0;
    for (int i = 0; i < 1 << n; i++) { //容斥原理的项数,0000 代表一个限制条件都没有, 0001代表第1个限制条件生效,0011,代码第1,2个限制条件生效
        LL a = m + n - 1, b = n - 1;   //上限是m+n-1,下限不一样
        int sign = 1;                  //奇数个限制条件,需要减;偶数个限制条件,需要加。现在这种限制条件组合状态,是奇数个限制,还是偶数个限制?
        for (int j = 0; j < n; j++)    //枚举状态的每一位
            if (i >> j & 1) {          //如果此位是1
                sign *= -1;            //符号位变号,从1变为-1,或者,从-1变为1; 这个用法挺牛X的,漂亮的实现了奇数次负号,偶数次正号的维护
                a -= A[j] + 1;         //拼公式
            }
        res = (res + C(a, b) * sign) % mod;
    }
    cout << (res + mod) % mod << endl;
    return 0;
}

标签:隔板,214,int,res,LL,小球,Devu,AcWing,mod
来源: https://www.cnblogs.com/littlehb/p/16378751.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有