组合数学学习笔记(三):生成函数

形式幂级数

考虑普通多项式 i=0naixi,将 n 的范围扩大到无穷,就会得到一个无穷多项式 i0aixi,这个东西就叫做形式幂级数。于是,普通的多项式可以看作从 n+1 开始系数全为 0 的形式幂级数。

形式幂级数的加法和减法同普通多项式一样,也就是 i0aixi±i0bixi=i0(ai±bi)xi

形式幂级数的乘法可以通过乘法分配律来推导,可以发现形式幂级数的乘法就是这两个数列的卷积 i0(j=0iajbij)xi。因此,也可以发现形式幂级数的单位元就是 i0[i=0]xi,也就是 1

考虑形式幂级数求逆,也就是我们在已知形式幂级数 F(x) 的情况下要求出另一个形式幂级数 G(x) 满足 F(x)G(x)=1,于是我们要求 f0g0=1,j=0ifjgij=0(i>0),将 gi 这一项单独提出来可以得到 gif0=j=1ifjgij,最终可以化简成 gi=g0j=1ifjgij,这样就可以通过 O(n2) 递推求出 gn,如果套用牛顿迭代,则可以在 O(nlog2n) 的时间复杂度内求出 gn。此时,我们也知道了形式幂级数 F(x) 存在逆的充要条件是 g00

现在我们就知道如何对形式幂级数做除法了,考虑 F(x)G(x),也就是 F(x)G(x)1,假设 G(x)1=H(x),那么 H(x)=i0(h0j=1igjhij)xi,于是答案 R(x) 就是 i0(ri={f0g01i=0(fij=1igjrij)×g01i<dg01j=1dgjrijid)xi

形式幂级数的求导与积分就与普通多项式一样了,这里不再赘述。

关于为何学习形式幂级数,以下是大神张ql 的见解,比较赞同:

当我在学OI的时候,曾经有人把多项式比作【数据删除】,我想这的确是一种污名化。的确,过度考察 FFT,多项式全家桶的确会让 OI 变成“默写竞赛”。但其实一些组合计数题目通过形式幂级数的手段,会得到更“有结构的”(structured) 的做法,会看到更“形象”的解释。以管窥豹。

形式幂级数把组合数学(生成函数)与多项式结合起来了,这也算是数学的一种融合。

普通生成函数(OGF

A generating function is a device somewhat similar to a bag.

Instead of carrying many little objects detachedly, which could be embarrassing,

we put them all in a bag.

And then, we have only one object to carry, the bag.

——George Polya

其实,一个数列也可以表示一个函数,但有些时候,用一个函数来计数要方便一些,这时我们就需要用到生成函数。很显然,普通生成函数就是一种形式幂级数。

定义一个无穷数列 h0,h1,h2,,hn, 的生成函数 g(x)h0+h1x+h2x2++hnxn+

而一个有限的数列,可以通过在后面补 0 的方式转化为无穷数列,这样就都可以用生成函数了。

普通数列的生成函数

这些数列一般只用掌握一些式子的变换或离散微积分就可以求出该数列的生成函数,比如:

  • {1,0,0,}1

  • {1,1,1,}1+x+x2+,根据等比数列求和公式,原式 =11x

  • {1,2,3,}1+2x+3x2+

我们发现这个式子不太好求和,考虑到 1+x+x2+=11x

对两边求导,可以得到 ddx(1+x+x2+)=ddx(11x)

求出两边的式子,根据多项式求导公式(见多项式学习笔记(一)(2024.7.6)fi=(i+1)fi+1,可以得到 1+2x+3x2+=1(1x)2

那么原数列的生成函数就是 1(1x)2

  • {1,p,p2,}1+px+p2x2+,将 px 看成一个整体,那么这与第 2 的数列就一模一样了,因此这个数列的生成函数为 11px

  • {(n0),(n1),(n2),}(n0)+(n1)x+(n2)x2+,发现这是一个二项式定理的形式,因此这个数列的生成函数为 (1+x)n

此时我们发现,我们用一个较为简单的函数表示了一个复杂的无穷数列,但是,就第 2 个数列的生成函数而言,当我们取 x1 时,发现两边的式子根本不相等,只是通过一系列运算法则和公式推导出来的,这时,这个生成函数就被叫做形式幂级数,化简后的结果就叫做这个生成函数的封闭形式。

练习一

an=f(n) 的生成函数的封闭形式,其中 f(x)=i=0k1fixi

解:原式的生成函数 =i0xij=0k1fjij

根据斯特林数的展开式,可以得到:i0xij=0k1fjt=0j{jt}it

将所有求和外移,可以得到:i0j=0k1t=0jxifj{jt}it

将求和顺序调整,可以得到:t=0k1j=tk1fj{jt}i0xiit

那么现在就需要对 i0xiit 求生成函数。

由于当 i<t 时,下降幂会乘到 0,对答案没有贡献,那么可以将原式改写成 itxiit

我们已知 fi=(i+1)fi+1,那么对 1+x+x2+x3++xt+ 求一次导就等于 1+2x+3x2++txt+,求两次导就等于 2+6x+12x2++t(t+1)xt,可以发现求 t 次导就变成 1t+2tx+3tx2++ttxt,将其乘以 xt,并将上升幂转为下降幂,可以得到 ttxt+(t+1)txt+1+(t+2)txt+2++(2t)tx2t+=itxiit。有点凑的感觉,但是如果掌握了多项式求导,那么还是可以算出来的。根据之前求出的 1+x+x2+x3++xt+ 的生成函数,itxiit 的生成函数就是 xt(11x)(t)

现在再切换回复合函数的求导,那么该生成函数就为 t!xt(1x)t+1=(1)txt(1x)t+1

那么原式就可以成 t=0k1xt(1)t(1x)t+1j=tk1fj{jt},此时式子已经与 i 无关,是原函数的封闭形式。

递推数列的生成函数

一个经典的递推函数就是斐波那契数列,它满足 fi={1i=12fi1+fi2i>2,它的通项公式是可以求出来的,主要有生成函数与线性代数两种解法,这里只讨论生成函数的做法。

首先可以写出斐波那契数列的前几项:{1,1,2,3,5,8,},然后写出它的生成函数:1+x+2x2+3x3+5x4+8x5+

设这个生成函数为 A,那么 xA=x+x2+2x3+3x4+5x5+8x6+x2A=x2+x3+2x4+3x5+5x6+8x7+,将两个式子加在一起可以得到 xA+x2A=x+2x2+3x3+5x4+8x5+,再用 A 减掉这个式子就可以得到 AxAx2A=1,因此 A=11xx2

这个式子特别复杂,考虑能否把这个生成函数转为之前求出的几个简单的生成函数的和,因为这些生成函数所对应数列的通项公式很容易知道。

将分母转化为 (1ϕ1x)(1ϕ2x),其中 ϕ1=1+52,ϕ2=152,将原式裂项,变成 ϕ15(1ϕ1x)ϕ25(1ϕ2x),这时,我们发现 1(1ϕ1x) 所对应的数列是 {1,ϕ1,ϕ12,ϕ13,},这个数列的第 n 项为 ϕ1n11(1ϕ2x) 所对应的数列是 {1,ϕ2,ϕ22,ϕ23,},这个数列的第 n 项为 ϕ2n1,那么原数列,也就是斐波那契数列的第 n 项就为 ϕ15ϕ1n1ϕ25ϕ2n1=15(ϕ1nϕ2n)=15[(1+52)n(152)n]

按照这个方法,按理说可以求出所有递推数列的通项公式,但根据迦罗瓦理论,五次即五次以上的代数方程不存在根式解,即我们无法求出 ϕ1,ϕ2,ϕ3,,ϕn(n5),不过我们可以通过常系数齐次线性递推的方式来求解递推数列第 n 项的值,不过太难了,以后再学。

生成函数的应用

考虑数列 h0,h1,h2,,hn,,其中 hn 表示 e1+e2++ek=n 的非负整数解的数目,那么这就变成了将 n 个不可区分小球放入 k 个可区分的盒子,每个盒子可以为空的方案数。

考虑到先给每个盒子里装 1 个小球,那么每个盒子就不能为空了,此时有 n+k 个物品了,考虑插板法,那么就是在 n+k1 个空中选 k1 个,方案数为 (n+k1k1),此时这个数列的生成函数为 i0(i+k1k1)xi=1(1x)k

此时你会发现,1(1x)k=11x×11x××11xk1=(1+x+x2+)(1+x+x2+)(1+x+x2+)=(e10xe1)(e20xe2)(ek0xek),于是,单个物品能取到的数所形成数列的生成函数之积 等于所有物品能取到的数所形成数列的生成函数。

这个性质再求某些带限制的背包问题中有非常大的用处,下面举两个例子。

例题一

各种食物的生成函数如下:

  • 承德汉堡:{1,0,1,0,1,0,}=1+x2+x4+x6+=11x2

  • 可乐:{1,1,0,0,0,0,}=1+x

  • 鸡腿:{1,1,1,0,0,0,}=1+x+x2

  • 蜜桃多:{0,1,0,1,0,1,}=x+x3+x5+x7+=x1x2

  • 鸡块:{1,0,0,0,1,0,}=1+x4+x8+x12+=11x4

  • 包子:{1,1,1,1,0,0,}=1+x+x2+x3

  • 土豆片炒肉:{1,1,0,0,0,0,}=1+x

  • 面包:{1,0,0,1,0,0,}=1+x3+x6+x9+=11x3

全部乘起来为 x1x4,它是 i0xi+1(i+33)=i1xi(i+23) 的封闭形式,于是 (n+23) 就是这个问题的答案。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MOD = 10007;
int extend_gcd(int a, int b, int &x, int &y){
	if(b == 0){
		x = 1;
		y = 0;
		return 0;
	}
	int d = extend_gcd(b, a % b, y, x);
	y -= a / b * x;
	return d;
}
int mod_inverse(int a, int m){
	int x, y;
	extend_gcd(a, m, x, y);
	return (x % m + m) % m;
}
int n;
char c;
signed main(){
	while(isdigit(c = getchar()))
		n = (n * 10 + c - '0') % MOD;
	int inv = mod_inverse(6, MOD);
	printf("%lld", n * (n + 1) * (n + 2) * inv % MOD);
	return 0;
}

例题二

假设每个物品体积为 1,于是每个物品只能取 vi 倍数个,于是每个物品的生成函数就是 11xvi,总的方案数序列的生成函数就是 i=1n11xvi

下面就考虑各位的推式子能力了,由于求积不太能计算,于是考虑先多项式 ln 再多项式 exp,那么求积就变成了求和:exp[i=1nln(11xvi)]

ln(11xvi) 先求导再做积分,根据复合函数求导,原式变成了 exp[i=1n(11xvi)(1xvi)dx]

将式子化简,可以得到 exp[i=1nvixvi11xvidx]

发现 vixvi11xvi 和等比数列的和很像,展开后变成 exp[i=1nj1vixjvi1dx]

根据幂函数的积分,原式最终等于 exp(i=1nj0xjvij)

里面的多项式是好求的,再套个 exp 就可以了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 9, MOD = 998244353;
int rev[N], in[N];
void init(){
	in[1] = 1;
	for(int i = 2; i <= N; i++)
		in[i] = (MOD - MOD / i) * in[MOD % i] % MOD;
}
int qpow(int x, int y){
	int res = 1;
	while(y){
		if(y & 1)
			res = res * x % MOD;
		x = x * x % MOD;
		y >>= 1;
	}
	return res;
}
void NTT(int *f, int n, int opt){
	for(int i = 0; i < n; i++){
		rev[i] = rev[i >> 1] >> 1;
		if(i % 2 == 1)
			rev[i] |= n >> 1;
	}
	for(int i = 0; i < n; i++)
		if(i < rev[i])
			swap(f[i], f[rev[i]]);
	for(int h = 2; h <= n; h <<= 1){
		int mi = h >> 1;
		int gn = qpow(3, (MOD - 1) / h);
		for(int j = 0; j < n; j += h){
			int g = 1;
			for(int k = 0; k < mi; k++){
				int tmp = f[j + k + mi] * g % MOD;
				f[j + k + mi] = (f[j + k] - tmp + MOD) % MOD;
				f[j + k] = (f[j + k] + tmp) % MOD;
				g = g * gn % MOD;
			}
		}
	}
	if(opt == -1){
		reverse(f + 1, f + n);
	    int inv = qpow(n, MOD - 2);
	    for(int i = 0; i < n; i++)
			f[i] = f[i] * inv % MOD;
	}
}
int tmp[N << 1];
void inv(int *f, int *g, int n){
	if(n == 1){
		g[0] = qpow(f[0], MOD - 2);
		return;
	}
	inv(f, g, (n + 1) >> 1);
	int lim = 1;
	while(lim < (n << 1))
		lim <<= 1;
	for(int i = 0; i < n; i++)
		tmp[i] = f[i];
	for(int i = n; i < lim; i++)
		tmp[i] = 0;
	NTT(tmp, lim, 1);
	NTT(g, lim, 1);
	for(int i = 0; i < lim; i++)
		g[i] = (2 - g[i] * tmp[i] % MOD + MOD) % MOD * g[i] % MOD;
	NTT(g, lim, -1);
	for(int i = n; i < lim; i++)
		g[i] = 0;
}
int f2[N << 1];
void ln(int *f, int *g, int n){
	for(int i = 0; i < (n << 2); i++)
		g[i] = 0;
	inv(f, g, n);
	int lim = 1;
	while(lim < (n << 1))
		lim <<= 1;
	for(int i = 0; i < n - 1; i++)
		f2[i] = f[i + 1] * (i + 1) % MOD;
	for(int i = n - 1; i < lim; i++)
		f2[i] = 0;
	NTT(f2, lim, 1);
	NTT(g, lim, 1);
	for(int i = 0; i < lim; i++)
		g[i] = g[i] * f2[i] % MOD;
	NTT(g, lim, -1);
	for(int i = n - 1; i > 0; i--)
		g[i] = g[i - 1] * in[i] % MOD;
	for(int i = n; i < lim; i++)
		g[i] = 0;
	g[0] = 0;
}
int lng[N << 1];
void exp(int *f, int *g, int n){
	if(n == 1){
		g[0] = 1;
		return;
	}
	exp(f, g, (n + 1) >> 1);
	ln(g, lng, n);
	int lim = 1;
	while(lim < (n << 1))
		lim <<= 1;
	for(int i = 0; i < n; i++){
		if(f[i] >= lng[i])
			lng[i] = f[i] - lng[i];
		else
			lng[i] = f[i] - lng[i] + MOD;
	}
	for(int i = n; i < lim; i++)
		lng[i] = g[i] = 0;
	lng[0]++;
	NTT(lng, lim, 1);
	NTT(g, lim, 1);
	for(int i = 0; i < lim; i++)
		g[i] = g[i] * lng[i] % MOD;
	NTT(g, lim, -1);
	for(int i = n; i < lim; i++)
		g[i] = 0;
}
int v[N], cnt[N], f[N], g[N], n, m;
signed main(){
	init();
	scanf("%lld%lld", &n, &m);
	for(int i = 1; i <= n; i++){
		scanf("%lld", &v[i]);
		cnt[v[i]]++;
	}
	for(int i = 1; i <= m; i++)
		if(cnt[i])
			for(int j = i; j <= m; j += i)
				f[j] = (f[j] + cnt[i] * in[j / i]) % MOD;
	exp(f, g, m + 1);
	for(int i = 1; i <= m; i++)
		printf("%lld\n", g[i]);
	return 0;
}

指数生成函数(EGF)

考虑泰勒展开的式子 f(x)=i0f(i)(x0)i!(xx0)i,可以发现泰勒展开在普通生成函数的基础上,每一项都除以了 i!。因此,当普通生成函数比较难以化成封闭形式时,可以考虑将每一项除以 i!,再进行化简。于是,i0aii!xi 就被称为数列 {a0,a1,a2,} 的指数生成函数。

一些特殊数列的指数生成函数:

  • {1,1,1,}=i0xii!=ex,根据 ex 的泰勒展开式即可得出,这也是 e 名字的由来;

  • {1,a,a2,}=i0aii!xi=eax,也是根据 eax 的泰勒展开式得出的;

  • {P(n,0),P(n,1),P(n,2),},其中 P(n,k) 表示从 n 个元素的集合中选出 k 个数组成的排列的数量,可以知道 P(n,k)=nk,于是它的指数生成函数是 i0nii!xi=(1+x)n

考虑将下降幂写成阶乘形式,于是原式 =i0n!i!(ni)!xi,可以发现 n!i!(ni)! 就是 (ni),那么原式就会变成 i0(ni)xi,根据普通生成函数的计算,原始最终等于 (1+x)n

其实,我们已经发现指数生成函数与集合的排列数有关系,不过我们先来了解一下指数生成函数的计算。

运算

应用

多项式指数函数(exp)拓展

概率生成函数(PGF

狄利克雷生成函数(DGF

例题八

参考资料

posted @   JPGOJCZX  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示