[GESP202312 八级] 奖品分配的题解
【题目描述】
班上有\(N\)名同学,学号从\(0\)到\(N-1\)。有\(M\)种奖品要分给这些同学,其中,第\(i\)种奖品共有\(ai\)个(i=0,1,2...M-1)。
巧合的是,奖品的数量不多不少,每位同学都可以恰好分到一个奖品,且最后剩余的奖品不超过\(1\)个(即:N <= a0+a1+a2+...+aM-1<=N+1)。
现在,请你求出每个班级礼物分配的方案数,所谓方案,指的是为每位同学都分配一个种类的奖品。
只要有一位同学获得了不同种类的奖品,即视为不同的方案。方便起见,你只需要输出方案数对\(10^9+7\)取模后的结果即可。
共有\(T\)个班级都面临着奖品分配的问题,你需要依次为他们解答。
【输入格式】
第一行一个整数\(T\),表示班级数量。
接下来是\(T\)行,每行若干用单个空格隔开的正整数。首先是两个正整数\(N\), \(M\),接着是\(M\)个整数a0,a1...aM-1。保证\(N\) <= a0+a1+a2+...+aM-1<=\(N+1\)。
【输出格式】
输出T行,每行一个整数,表示该班级分配奖品的方案数对\(10^9+7\)取模后的结果。
【输入输出样例#1】
输入 #1
3
3 2 1 2
3 2 1 3
5 3 1 3 1
输出 #1
3
4
20
输入 #2
5
100 1 100
100 1 101
20 2 12 8
123 4 80 20 21 3
999 5 101 234 499 66 99
输出 #2
1
1
125970
895031741
307187590
【说明提示】
样例解释 1
对于第1个班级,学号为0,1,2的同学可以依次分别获得奖品0,1,1,也可以依次分别获得奖品1,0,1,也可以依次分别获得奖品1,1,0,因此共有3种方案。
对于第2个班级,学号为0,1,2的同学可以依次分别获得奖品0,1,1,也可以依次分别获得奖品1,0,1,也可以依次分别获得奖品1,1,0,也可以依次分别获得奖品1,1,1,因此共有4种方案。
对于第3个班级,可以把编号为0的奖品分配给5名同学中的任意一名,共有5种方案;再把编号为2的奖品分配给剩余4名同学中的任意1名,共有4种方案;最后给剩余3名同学自然获得1号奖品。因此,方案数为\(5*4=20\)。
数据范围
对于\(30\)%的测试点,保证\(N\) <= 10。
对于另外\(30\)%的测试点,保证\(M\)<=2。
对于所有测试点,保证\(N\)<=1000;保证\(T\)<=1000;保证\(M\)<=1001。
思路
我们仔细看看样例解释的第三个班级,它的描述方式是不是很熟悉,对的,这就是一个很简单的组合数问题。这道题只有一个很麻烦的地方,就是有一种情况,所有的奖品数之和是等于N+1的。那我们就来分类讨论:
第一种情况(奖品数等于人数)
正合我们意,直接先用杨辉三角预处理出组合数,然后带入在剩余的\(N\)个人中选出ai个人,也就是\(C_{N}^{ai}\)(记得每次求出答案后要把\(N\)减去ai)代码如下:
for(int i = 1; i <= m; i++){
ans = ans * f[n][a[i]] % mod;
n -= a[i];
}
其中的f数组为组合数数组,记得取模!!
第二种情况(奖品数等于人数加一)
现在就有一个问题了,我们到底剩下那个奖品?考虑考虑就会知道,其实剩下的那个礼物我们是可以枚举的,但是时间复杂度呢?首先有T个问题,然后枚举剩下的那个奖品,最后在计算方案数,时间复杂度达到了惊人的\(O(N^3)\),我们看看\(N\)的大小,为1000,那么大小为\(O(10^9)\),这个时间复杂度那是肥肠的危险(但是本人试过,还是可以过,数据太水了!!吐槽),那我们考虑该如何优化时间复杂度。
其实认真想的话可以得知,这个操作也是非常的类似排列组合(就相当于在\(N\)个里面选择\(1\)个),那我们不如把他也放进我们一开始的组合数计算。那该怎么把他归并到一开始的组合数计算呢?很简单,只需要再加一个“虚拟的”人就行了。为什么这样可以呢?因为每个人有且只有一个奖品,那我们那个“虚拟的”人所拿到的奖品不就可以看成剩余的那个奖品吗?然后你就会发现,这不就是前面的组合数求解吗?(简直一摸一样)代码如下:
if(人数<奖品数)n ++;
for(int i = 1; i <= m; i++){
ans = ans * f[n][a[i]] % mod;
n -= a[i];
}
完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;//注意取模
const int N = 1010;
int T, a[N];
ll f[N][N];//组合数(杨辉三角)答案的存储数组
void init(int n){//杨辉三角求解,也就是求出组合数
for(int i = 0; i <= n; i++)
for(int j = 0; j <= i; j++){
if(i == j || j == 0) f[i][j] = 1;//判断边界
else f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % mod;//注意取模
}
}
void calc(){
int n, m; cin >> n >> m;
ll ans = 1;//答案
int sum = 0;//求出奖品总数
for(int i = 1; i <= m; i++){
cin >> a[i];
sum += a[i];
}
if(sum != n) n ++;//如果是第二种情况那么就加一个“虚拟的”人
for(int i = 1; i < m; i++){//组合数求解,用汉字表达就是在剩余的n个人中选出ai个人的方案数
ans = ans * f[n][a[i]] % mod;
n -= a[i];//这一步很重要,记得把已经选过的人减掉
}
cout << ans << "\n";//输出答案
}
int main(){
cin >> T;
init(1001);//注意了!我一开始也卡在这了,初始值一定要赋到1001,因为如果出现了人数不等于奖品数而且人数为1000的情况就会错
while(T --) calc();//T组数据求解方式
return 0;//goodbye!(撒花花!!)
}