难度解读
首先这个题分为了两种难度,而差别就只有n的不同,很多人只去看了难的那一个,虽然如果难的那一个会做了,简答题也就不攻自破了,但是既然这样设计了,那就代表着一些细节的处理上,简单的那个更好想而且更好做,所以本篇将针对难度来讲解两个做法
正片开始
分析一下题意,我们需要计算出这 2 n − 1 2^n - 1 2n−1的序列的权值和,先且不说计算出一个序列内所有数字乘积的权值需要多少时间,就这个全部序列遍历一遍就已经非常夸张了,所以我们一定有什么办法直接算出来
我们先来解决一个序列中权值的问题,由题意可得,序列中只会出现三个数字,分别是1、2、3,其实只要写几个数字看看就会发现,1对最后的权值没有贡献,也就是说1的数量在解决这个问题时不重要
2和3呢
举个例子就明白了
6的权值是4,也就是一个2和一个3相乘
那么3的能分解为 1 和 3
同样2能分解为1 和 2
有发现什么吗? 6分解出来的就是
1
∗
1
,
1
∗
3
,
2
∗
1
,
2
∗
3
1*1 , 1*3 , 2* 1 , 2 * 3
1∗1,1∗3,2∗1,2∗3
其实就是说一个序列的权值就是其中(2的数量 + 1) * (3的数量 + 1)
好了解决这个问题就到了我们最重要的如何避免遍历 2 n − 1 2^n - 1 2n−1次
其实也很简单,整个数组只可能出现三种数字,那也就是三种数字的排列组合再结合上面说的规律算出总数即可
可能这么说有点抽象,那么写出公式就是不难理解了
假设 num[1] = 1的数量
num[2] = 2的数量
num[3] = 3的数量
n 1 = 2 n u m [ 1 ] n1 = 2^{num[1]} n1=2num[1]
n 2 = ∑ i = 0 i = n u m [ 2 ] ( i + 1 ) ∗ C n u m [ 2 ] i n2 = \sum_{i = 0}^{i=num[2]}(i + 1) *C_{num[2]}^i n2=∑i=0i=num[2](i+1)∗Cnum[2]i
n 3 = ∑ i = 0 i = n u m [ 3 ] ( i + 1 ) ∗ C n u m [ 3 ] i n3 = \sum_{i = 0}^{i=num[3]}(i + 1) *C_{num[3]}^i n3=∑i=0i=num[3](i+1)∗Cnum[3]i
n1是因为1的不影响权值,无关紧要,只需要知道1的组合总数有多少就可以了,用二项式定理可得
n
1
=
∑
i
=
0
i
=
n
u
m
[
1
]
C
n
u
m
[
1
]
i
=
(
1
+
1
)
n
u
m
[
1
]
n1 = \sum_{i = 0}^{i=num[1]}C_{num[1]}^i = (1 + 1)^{num[1]}
n1=∑i=0i=num[1]Cnum[1]i=(1+1)num[1]
所以最后的结果就是n1 * n2 * n3 - 1, 减一是因为不可能都是0的情况
那为什么有两个难度
问题就在组合数怎么求上了
先来看F题
数据范围并不大,组合数就可以用动态规划的想法求出来
实际上就是 C n m = C n − 1 m − 1 + C n − 1 m C_n^m = C_{n-1}^{m-1} + C_{n-1}^{m} Cnm=Cn−1m−1+Cn−1m
代码如下
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1005 , mod = 1e9 + 7;
int n,t;
int num[4];
long long C[N][N];
void init()
{
for(int i=0;i<N;i++){
for(int j=0;j<=i;j++){
if(!j) C[j][i] = 1;
else C[j][i] = (C[j-1][i-1] + C[j][i-1]) % mod;
}
}
}
int qmi(int a,int b)
{
ll res = 1;
while (b){
if(b & 1) res = res * (ll)a % mod;
a = (ll)a * a % mod;
b >>= 1;
}
return res % mod;
}
int check(int j)
{
ll res = 0;
for(int i = 0; i <= num[j] ; i ++){
res = res % mod + ((C[i][num[j]] % mod) * (i+1));
}
return res % mod;
}
int main()
{
init();
cin >> n;
for(int i=0;i<n;i++){
cin >> t;
num[t] ++;
}
ll one,two,thr;
one = qmi(2,num[1]) % mod;
two = check(2) % mod;
thr = check(3) % mod;
cout << (((one * two) % mod * thr) % mod - 1 + mod) % mod << endl;
}
但是G题并不是的数据范围不能开一个二维数组啊,所以就需要从组合数的公式本身出发
C n m = n ! m ! ∗ ( n − m ) ! C_n^m = \frac{n!}{m! * (n-m)!} Cnm=m!∗(n−m)!n!
所以我们只需要遍历出来数据范围内所有的数字的阶乘就行了
看到这里你可能兴致冲冲的想要开始做了,但是我必须提醒一下的是
这个题是要求取模的,但是取模运算是不适用于除法的,也就是说不能单纯的对结果取个模就行了
难就难在这了
我们需要一个数学概念,逆元
就是
a
∗
a
−
1
≡
1
(
m
o
d
p
)
a * a^{-1} \equiv 1 (mod \quad p)
a∗a−1≡1(modp)
这里的
a
−
1
a^{-1}
a−1不是a的倒数,而是一个乘以a然后对p取余还能等于1的数字
而我们就是要算出数据范围内阶乘以及他的逆元
具体实现如下
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7, N = 2e5 + 5;
ll nums[4], n, t;
//快速幂
int qmi(int a,int b)
{
ll res = 1;
while (b){
if(b & 1) res = res * (ll)a % mod;
a = (ll)a * a % mod;
b >>= 1;
}
return res % mod;
}
//为了储存阶乘和逆元用的
ll f[N],inf[N];
void init()
{
f[0] = inf[0] = 1;
for(int i=1;i<N;i++){
f[i] = f[i - 1] * i % mod;
inf[i] = inf[i - 1] * qmi(i,mod-2) % mod;
}
}
//组合数计算
ll C(int a,int b)
{
return f[b] * inf[b-a] % mod * inf[a] % mod;
}
//算一共有多少种可能用的
int check(int o)
{
ll res = 0;
for(int i=0;i<=nums[o];i++){
res = (res + (i + 1) * C(i,nums[o]) % mod) % mod;
}
return res % mod;
}
int main()
{
init();
cin >> n;
while (n--){
cin >> t;
nums[t] ++;
}
ll one,two,thr;
one = qmi(2,nums[1]) % mod;
two = check(2) % mod;
thr = check(3) % mod;
//涉及到取模的减法运算一定别忘了要加一个mod,不然可能变成负数
cout << ((one * two) % mod * thr % mod + mod - 1) % mod << endl;
return 0;
}