洛谷·幼儿园篮球题【including范德蒙德卷积,二项式反演

初见安~时隔良久我又回来写多项式了【靠

还是放在题目前面吧,简单讲一下这两个东西。

一、范德蒙德卷积

\sum_{i=0}^kC_n^iC_{m}^{k-i}=C_{n+m}^k

可以理解为:在两个有n个石子和m个石子的堆里面共选k个石子的方案数。这样这个等式的成立就很显然了。
既然很显然为什么要讲?因为这个东西其实有的时候可以提醒你往这个方面去想【大雾。题中会用到

二、二项式反演

f_n=\sum_{i=0}^nC_n^ig_i\Leftrightarrow g_n=\sum_{i=0}^n(-1)^{n-i}C_{n}^if_i

我们可以理解为:求n个人错排的方案数。有点麻烦,我们可以先整一个f(x)表示x个人随便排的方案数,g(x)表示x个人的错排方案数,那么就有:

f_n=\sum_{i=0}^nC_n^ig_i

i枚举有多少个人是错排的,其余的人都是保证呆在原地的。这显然成立。那么同时也可以有:

g_n=\sum_{i=0}^n(-1)^{n-i}C_{n}^if_i

这里是个容斥,容斥至少n-i个人是在原地的。

所以就有二项式反演了。关于较详细的证明
有二项式定理是这样的:【由杨辉三角对称性可得】

\sum_{k=0}^n(-1)^kC_n^k=[n=0]

【方括号的含义和if一样,满足则为1,否则为0】

以及一个有点奇怪但很显然的扩充式子:

g_n=\sum_{m=0}^n[n-m=0]C_n^mg_m

于是我们可以将这里面的 [n-m=0] 与前式做个替换:

g_n=\sum_{k=0}^n(-1)^kC_n^k\sum_{m=0}^{n-k}C_{n-k}^mg_m

再一看:咦,这后面不就是这个嘛:

\sum_{i=0}^{n-k}C_{n-k}^ig_i=f_{n-k}

但是n-k看起来不舒服,所以我们替换成k,组合数里k和n-k的替换不影响,所以就有了:

g_n=\sum_{k=0}^n(-1)^{n-k}C_{n}^kf_k

证毕。

三、洛谷P2791 幼儿园篮球题

【这难道还没有个黑题的难度吗!!!超大声)】

题解

看起来很复杂,其实整理一下是很好理解的。S场篮球赛各自独立,也就相当于是S组数据。这里求期望我们完全可以老老实实把各种方案算出来然后除以总的情况数。也就是:【这里的k,n,m是对于该场篮球赛,意义如题】

\sum_{i=0}^kC_m^iC_{k-m}^{m-i}*i^L/C_n^k

哦——是不是就有点点像前面的范德蒙德卷积?那么这里的i^L就很碍眼了。有什么公式可以拿来替换呢——第二类斯特林数展开式。长这样的:【后面关于公示的化简都去掉了除以C_n^k的那个部分,懒得写】

S(i,j)=\frac{1}{j!}\sum_{k=0}^j(-1)^{j-k}C_j^kk^i

这里面就有一个k^i,看起来可以拿来替换。替换的方式就是简单移项然后套用我们前面的二项式反演,就可以得到:

k^i=\sum_{j=0}^kC_k^jS(i,j)*j!

\sum_{i=0}^kC_m^iC_{k-m}^{m-i}*i^L=\sum_{i=0}^kC_m^iC_{k-m}^{m-i}\sum_{j=0}^iC_i^jS(L,j)j!

好像动不了了。考虑范德蒙德卷积,我们后期应该会省掉i循环,所以把j循环提前——
这个步骤可以理解为一个对应关系,每个i都对应了所有小于等于它的j,那么相应的每个j也对应了所有大于等于它的i:

=\sum_{j=0}^kS(L,j)j!\sum_{i=j}^kC_m^iC_{k-m}^{m-i}C_i^j

关于S(L,j),因为第一位是全程固定了的,所以我们用NTT处理出j在L以内所有值的情况的值就可以了,展开式是可以卷的。那么问题就是后面的三个组合数。比较常见的,C_m^iC_i^j=C_m^jC_{m-j}^{i-j},前者可以理解为在m里面选i个,再在i个里面选j个,后者雷同,意义相同。这样我们就可以拆出一个与i无关的组合数。因为循环的下限有点奇怪,所以我们再变化一下,就可以直接套范德蒙德了:

\\ =\sum_{j=0}^kS(L,j)j!C_m^j\sum_{i=j}^{k}C_{k-m}^{m-i}C_{m-j}^{i-j}\\ =\sum_{j=0}^kS(L,j)j!C_m^j\sum_{i=0}^{k-j}C_{k-m}^{m-i-j}C_{m-j}^{i}\\ =\sum_{j=0}^kS(L,j)j!C_m^jC_{k-j}^{m-j}

好!到这里呢看起来好像这复杂度还是不可做。但是循环的上限真的是k吗?因为我们套了第二类斯特林数,所以j\leq L;因为后面有组合数,所以j\leq m。所以真正的循环上限为:min(k,m,L)。再带入一开始的总数C_n^k,把组合数拆开,预处理阶乘和逆元,这样的话整体的复杂度就是O(SL),完全可以过!!!

最后的式子就是:

ans=\sum_{j=0}^kS(L,j)*\frac{m!(n-j)!k!}{(m-j)!(k-j)!n!}

你以为这样就完了吗?并不!!!因为你会发现交上去后满屏的MLE或者RE。
因为很明显,阶乘和逆元的预处理数组必须开2e7以上,但你用了longlong你就没了。【所以这题的本质是卡内存。
所以只能开int并且在计算过程中全部加一个1ll*,表示计算转longlong,但是最后是int存下来。

嗯。就这样。【怕不是就我会忽略这种sb坑

上代码——

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 600005//这里要开大点
#define maxm 20000010
using namespace std;
typedef long long ll;
const int mod = 998244353;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

int N, M, S, L;
int fac[maxm], inv[maxm];
ll pw(ll a, ll b) {ll res = 1; while(b) {if(b & 1) res = res * a % mod; a = a * a % mod, b >>= 1;} return res;}

int len = 1, l = 0, r[maxn];
void NTT(int *c, int flag) {
	for(int i = 1; i <= len; i++) if(i < r[i]) swap(c[i], c[r[i]]);
	for(int mid = 1; mid < len; mid <<= 1) {
		ll gn = pw(3, (mod - 1) / (mid << 1));
		if(flag == -1) gn = pw(gn, mod - 2);
		for(int ls = 0, L = mid << 1; ls < len; ls += L) {
			ll g = 1;
			for(int k = 0; k < mid; k++, g = g * gn % mod) {
				ll x = c[ls + k], y = g * c[ls + mid + k] % mod;
				c[ls + k] = 1ll * (x + y) % mod, c[ls + mid + k] = 1ll * (x - y + mod) % mod;
			}
		}
	}
	ll rev = pw(len, mod - 2);
	if(flag == -1) for(int i = 0; i <= len; i++) c[i] = 1ll * c[i] * rev % mod;
}

int F[maxn], G[maxn];
int n, m, k;
signed main() {
	N = read(), M = read(), S = read(), L = read();
	
	fac[0] = inv[0] = 1; int mx = max(N, L);
	for(int i = 1; i <= mx; i++) fac[i] = 1ll * fac[i - 1] * i % mod;
	inv[mx] = pw(fac[mx], mod - 2);
	for(int i = mx - 1; i > 0; i--) inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
	
	for(int i = 0, kd = 1; i <= L; i++, kd = -kd) G[i] = (1ll * kd * inv[i] % mod + mod) % mod;
	for(int i = 0; i <= L; i++) F[i] = 1ll * inv[i] * pw(i, L) % mod;
	//F和G是为了NTT第二类斯特林数的
	while(len <= L + L) len <<= 1, l++;
	for(int i = 1; i <= len; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) << l - 1);
	NTT(F, 1), NTT(G, 1);
	for(int i = 0; i <= len; i++) F[i] = 1ll * F[i] * G[i] % mod;
	NTT(F, -1);
	
	ll ans;
	while(S--) {
		n = read(), m = read(), k = read(); 
		register int lim = min(k, min(m, L)); ans = 0;
		for(int i = 0; i <= lim; i++) //这里就看前面的公式推导即可
                    ans = (ans + F[i] * 1ll * fac[n - i] % mod * inv[m - i] % mod * inv[k - i] % mod) % mod;
		printf("%lld\n", 1ll * ans * fac[m] % mod * fac[k] % mod * inv[n] % mod);
	}
	return 0;
}

内容有点多呀。迎评:)
——End——

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值