组合数学学习笔记(三):生成函数
形式幂级数
考虑普通多项式
形式幂级数的加法和减法同普通多项式一样,也就是
形式幂级数的乘法可以通过乘法分配律来推导,可以发现形式幂级数的乘法就是这两个数列的卷积
考虑形式幂级数求逆,也就是我们在已知形式幂级数
现在我们就知道如何对形式幂级数做除法了,考虑
形式幂级数的求导与积分就与普通多项式一样了,这里不再赘述。
关于为何学习形式幂级数,以下是大神张ql 的见解,比较赞同:
当我在学OI的时候,曾经有人把多项式比作【数据删除】,我想这的确是一种污名化。的确,过度考察 FFT,多项式全家桶的确会让 OI 变成“默写竞赛”。但其实一些组合计数题目通过形式幂级数的手段,会得到更“有结构的”(structured) 的做法,会看到更“形象”的解释。以管窥豹。
形式幂级数把组合数学(生成函数)与多项式结合起来了,这也算是数学的一种融合。
普通生成函数( )
其实,一个数列也可以表示一个函数,但有些时候,用一个函数来计数要方便一些,这时我们就需要用到生成函数。很显然,普通生成函数就是一种形式幂级数。
定义一个无穷数列
而一个有限的数列,可以通过在后面补
普通数列的生成函数
这些数列一般只用掌握一些式子的变换或离散微积分就可以求出该数列的生成函数,比如:
-
; -
,根据等比数列求和公式,原式 ; -
。
我们发现这个式子不太好求和,考虑到
对两边求导,可以得到
求出两边的式子,根据多项式求导公式(见多项式学习笔记(一)(2024.7.6))
那么原数列的生成函数就是
-
,将 看成一个整体,那么这与第 的数列就一模一样了,因此这个数列的生成函数为 ; -
,发现这是一个二项式定理的形式,因此这个数列的生成函数为 。
此时我们发现,我们用一个较为简单的函数表示了一个复杂的无穷数列,但是,就第
练习一
求
解:原式的生成函数
根据斯特林数的展开式,可以得到:
将所有求和外移,可以得到:
将求和顺序调整,可以得到:
那么现在就需要对
由于当
我们已知
现在再切换回复合函数的求导,那么该生成函数就为
那么原式就可以成
递推数列的生成函数
一个经典的递推函数就是斐波那契数列,它满足
首先可以写出斐波那契数列的前几项:
设这个生成函数为
这个式子特别复杂,考虑能否把这个生成函数转为之前求出的几个简单的生成函数的和,因为这些生成函数所对应数列的通项公式很容易知道。
将分母转化为
按照这个方法,按理说可以求出所有递推数列的通项公式,但根据迦罗瓦理论,五次即五次以上的代数方程不存在根式解,即我们无法求出
生成函数的应用
考虑数列
考虑到先给每个盒子里装
此时你会发现,
这个性质再求某些带限制的背包问题中有非常大的用处,下面举两个例子。
各种食物的生成函数如下:
-
承德汉堡:
; -
可乐:
; -
鸡腿:
; -
蜜桃多:
; -
鸡块:
; -
包子:
; -
土豆片炒肉:
; -
面包:
。
全部乘起来为
点击查看代码
#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;
}
假设每个物品体积为
下面就考虑各位的推式子能力了,由于求积不太能计算,于是考虑先多项式
将
将式子化简,可以得到
发现
根据幂函数的积分,原式最终等于
里面的多项式是好求的,再套个
点击查看代码
#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)
考虑泰勒展开的式子
一些特殊数列的指数生成函数:
-
,根据 的泰勒展开式即可得出,这也是 名字的由来; -
,也是根据 的泰勒展开式得出的; -
,其中 表示从 个元素的集合中选出 个数组成的排列的数量,可以知道 ,于是它的指数生成函数是 ;
考虑将下降幂写成阶乘形式,于是原式
其实,我们已经发现指数生成函数与集合的排列数有关系,不过我们先来了解一下指数生成函数的计算。
运算
应用
多项式指数函数( )拓展
概率生成函数( )
狄利克雷生成函数( )
参考资料
-
Introductory Combinatorics(Fifth Edition) Richard A. Brualdi
-
Concrete Mathmatics(Second Edition) Donald L.Graham, Donald K.Knuth and Oren Patashnik
-
生成函数封闭形式的正确理解 Sunlight-zero
-
利用生成函数求斐波那契数列通项公式 zwfymqz
-
生成函数封闭形式的正确理解 Sunlight-zero
-
多项式计数杂谈 command_block
本文来自博客园,作者:JPGOJCZX,转载请注明原文链接:https://www.cnblogs.com/JPGOJCZX/p/18556797
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具