CF偶遇Ultra-Meow,阶乘爆炸强如怪物,拼尽全力无法战胜
看完答案之后豁然开朗,但是答案的代码比较抽象,我试着用形象的说法来讲解一下思路
刻下的话语,浮现心头:将我的思路,弃置于此
问题描述:
将题目翻译一下,意思大致是:定义一个运算MEX(),再给你一个数,比如4,对应的集合就是{1,2,3,4},5就是1~5,以此类推,要求出这个集合所有子集的MEX,我试着用比较形象的方式来描述一下个MEX运算:
假设这个表格是全集1~10:X表示该位置对应的数字存在集合中
X | X | X | X | X | X | X | X | X | X |
那么给的集合A是这样的:元素个数为N
X | X | X | X |
进行的MEX运算就是,再给你N+1个O来从左往右填剩下的表格,最后能到达的位置就是你的输出:
可以看到,O最后到达了第九个格子,对应的数字就是9
X | O | X | O | X | X | O | O | O |
所以MEX(A)的输出就是9
特别注意:最后输出的是O所能到达的位置,跟X最远在哪里无关;X是不能超出给定集合的范围的(因为X是集合子集的元素),但是O可以,因为那是给你拿去填的东西;同样,X和X之间可以有空隙,但是O和X,O和O之间不能有空隙
这种情况是允许的:
X | O | O | O | X | O | O | X | X |
这样是不可以的:
X | O | O | X | O | O | X | O | X | O |
最简单的描述就是:给你N个固定的X和N+1个O,O在不留空的情况下最远能填到多少
如果还是理解不了,可以继续往下看,有具体例子应该会更好看懂
这样就很好理解了,那么如何解决这个问题呢:
首先,输入的是一个数字N,代表集合S的大小,集合元素规定是从1,2,3,4....N,但我们要求的不是MEX(S),而是S所有子集的MEX的和,所以我们要从如何枚举所有S的子集入手
但是我们并不需要知道每个子集都是什么,原因如下:
继续按照我们填格子的思路,对集合S={1,2,3,4,5,6,7,8,9,10}的子集A={1,3,4,5,7,}和子集B={2,3,5,7,8}来说情况是一样的,其实和最简单的五个元素的子集C={1,2,3,4,5}都是一样的
X | O | X | X | X | O | X | O | O | O | O |
O | X | X | O | X | O | X | X | O | O | O |
X | X | X | X | X | O | O | O | O | O | O |
可以看到,我们最后都到达了11,这就引申出两种情况了 :即子集的大小是否达到了给定集合大小的一半,如果达到了一半及以上,那么很容易得到:X不管在格子里如何排列,O都能填上它们之间的空隙,并再往后面填,所以,O最远能到达的位置一定是N+N+1,举个例子:输入为10时,对它所有大小在5及以上的子集来说,比如大小为5的子集,都有5个X和6个O能填,那么不管X在前十个格子里如何排列,O一定都能把十个格子填满再往后填一个,所以对于所有大小在5以上的子集来说,MEX的输出都是11
那么一共有多少大小在5以上的子集呢?
这里就要用到组合数,C(N,I),即在N个数里选I个一共有几种方式,那么易得大小为5的子集数量是:C(10,5) (具体的计算代码在代码部分)
基于这样的思路,很容易想到要从子集大小入手来遍历所有的子集,那么大小大于输入一半的子集很好计算,小于输入一半的子集就有点复杂了:我们来看这几种情况
比如,输入为10时,大小为3的子集
X | O | O | X | O | O | X |
X | O | O | O | O | X | X |
O | O | O | O | X | X | X |
X | X | X | O | O | O | O |
这四个例子中,MEX的输出分别为:6,5,4,7(要记得,我们需要的是O最后到达的位置,而不是X)这里就来到整个算法最绕的地方了,只要能理解到,整个代码的难度就只剩下对于组合数计算时阶乘太大的问题了
我们可以先忘记X和O的区别,我们只看X和O的数量,可知总数量对于每个集合来说都是确定的2*N+1,我们最后要的是一个由X和O串起来的链子(大家可以想象一下),且链子的最后一个一定是O,我们要的就是这个链子的长度(不一定每个X都被串在了链子里)
最简单,也是最理想的情况
X | X | X | O | O | O | O |
同样,这个情况和上面是完全一致的:
X | O | X | O | X | O | O |
也就是说,不关链子内部被如何打乱,只要串进去的X数目一样,最后的结果都是一样的(下面同理)(在链子末端的X视为没有串进去,因为链子的结尾必须是O)
O到达了最远的7
那如果有一个X跑到了7之外呢(也就是没有被串在链子里,在这种情况里因为元素太少,X还有空位可以待,如果是上面讨论的大于一半情况那么X无论待在哪里都会被串进去)
X | X | O | O | O | O | X |
我们的O最远就只能到6了
以此类推如果有两个X跑到了外面。。。
X | O | O | O | O | X | X |
最后的输出就只有5
所以对于这个部分我们可以这样来计算,我们假设最后串起来的链子长度为K,而子集的大小为I,输入的全集大小为N
那么我们要遍历有没有一个X在外面的情况,一个X在外面的情况,两个X在外面的情况.....所有X都在外面的情况,那么用for循环就可以达成K从I+1到2*I+1的遍历(K=I+1对应所有X都在外面的情况,K=2*I+1对应所有X都被串进去的情况)
那么,胜利就在眼前了,对于一个大小为I的子集,链子的长度为K(也就是我们要的MEX输出),那么怎么计算一共有几种排列方式呢,从给定的数据可以看出,串进链子的X个数应该是链子长度-O的个数,即K-(I+1);在链子外面的X个数应该是子集大小-链子中X的个数,即2*I-K-1,那么我们就是要在链子中选K-(I+1)个X,在链子外选2*I-K-1个X,即C(K-1,K-(I+1))和C(N-I,2*I+1-K)
特别注意第一个C(K-1,K-(I+1)),应该是K-1而不是K的原因是在链子中选X的时候不可以选最后一个位置,因为那里必须是O,所以其实是在K-1个位置里选(我自己写代码的时候就忽略了这一点所以一直无法通过测试,看了一个上午才发现这里的错误)
至此,整个算法的大体思路就结束了,下面是代码实现和一些细节上的东西
1.组合数计算时涉及阶乘的运算,在测试例4999上会超出int的范围
其实题目的下面有一行字来提示我们要取模运算,但我做题的时候忽略了,加上我之前也没有了解过取模运算这种方式
答案中给出了取模运算以及之后进行阶乘,组合数运算的代码,还是相当好理解的
const int mod = 1e9 + 7;
int add(int a, int b) {
if (a + b >= mod)
return a + b - mod;
return a + b;
}
int sub(int a, int b) {
if (a < b)
return a + mod - b;
return a - b;
}
int mul(int a, int b) {
return (int)((1ll * a * b) % mod);
}
int binpow(int a, int pw) {
int b = 1;
while (pw) {
if (pw & 1)
b = mul(b, a);
a = mul(a, a);
pw >>= 1;
}
return b;
}
int inv(int a) {
return binpow(a, mod - 2);
}
const int N = 15000;
int f[N], r[N];
void precalc() {
f[0] = 1;
for (int i = 1; i < N; i++)
f[i] = mul(f[i - 1], i);
r[N - 1] = inv(f[N - 1]);
for (int i = N - 2; i > -1; i--)
r[i] = mul(r[i + 1], i + 1);
}
int C(int n, int k) {
if (n < 0 || k < 0 || n < k)
return 0;
return mul(f[n], mul(r[k], r[n - k]));
}
取模运算可以在保留数字的一些数学特性的前提下,让数据不超过某一个特定的范围,在本题的计算中主要用于计算阶乘和组合数,并不会影响最后计算得到的数值的绝对大小(因为我们需要的只是排列的个数,阶乘的数值经过取模之后不会影响我们计算得到的组合数大小)(这一块我也不是很了解,甚至可能现在说的也不一定对,数学功底不太好是这样的)
答案中的代码还运用了预处理的方式来减少运行时间,不然每次都重新进行阶乘的计算会相当费时
下面附上我写的代码(其实跟照抄标准答案没什么区别了,自己写的时候没写取模运算的部分,直接复制的标准答案的取模代码,后面的内容也都大同小异)
#include<iostream>
using namespace std;
const int mod = 1e9 + 7;
int mul(int a, int b) {
return (int)((1ll * a * b) % mod);
}
int binpow(int a, int pw) {
int b = 1;
while (pw) {
if (pw & 1)
b = mul(b, a);
a = mul(a, a);
pw >>= 1;
}
return b;
}
int inv(int a) {
return binpow(a, mod - 2);
}
const int N = 15000;
int f[N], r[N];
void precalc() {
f[0] = 1;
for (int i = 1; i < N; i++)
f[i] = mul(f[i - 1], i);
r[N - 1] = inv(f[N - 1]);
for (int i = N - 2; i > -1; i--)
r[i] = mul(r[i + 1], i + 1);
}
int C(int n, int k) {
if (n < 0 || k < 0 || n < k)
return 0;
return mul(f[n], mul(r[k], r[n - k]));
}
int add(int a, int b) {
if (a + b >= mod)
return a + b - mod;
return a + b;
}
int MEOW(int n)
{
int sum = 1;
for (int i = 1; i <= n; i++)
{
if (2 * i >= n)
{
sum=add(sum,mul((2 * i + 1), C(n, i)));
}
else
{
for (int k = i+1; k <= 2*i+1; k++)
{
sum =add(sum, mul(k , mul(C(k-1,k-i-1), C(n-k, 2*i+1-k))));
}
}
}
return sum;
}
int main()
{
precalc();
int m = 0;
cin >> m;
for (int i = 0; i < m; i++)
{
int n = 0;
cin >> n;
cout << MEOW(n) << endl;
}
return 0;
}