洛谷·幼兒園籃球題【including範德蒙德卷積,二項式反演

初見安~時隔良久我又回來寫多項式了【靠

還是放在題目前面吧,簡單講一下這兩個東西。

一、範德蒙德卷積

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

可以理解爲:在兩個有n個石子和m個石子的堆裏面共選k個石子的方案數。這樣這個等式的成立就很顯然了。
既然很顯然爲什麼要講?因爲這個東西其實有的時候可以提醒你往這個方面去想【大霧。題中會用到

二、二項式反演

它有兩個優美的形式:

\small \dpi{150} f_n=\sum_{i=0}^n(-1)^iC_{n}^ig_i\Leftrightarrow g_n=\sum_{i=0}^n(-1)^iC_{n}^if_i

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——

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章