专栏:再来一遍一定记住的算法_proMatheus的博客-CSDN博客
温馨提示:推导不难,请同学们跟着文章推导完后,自己再在纸上写一遍。
在学习之前,你需要知道:
二项式:只有两项的多项式。
导数(高中知识啦,不会去问AI or 上b站看一数)
组合数,从
个里面选
个,不在意顺序。
收敛性:当输入的值 (比如 )趋近某个点时,
且函数的输出值 ()越来越接近某个固定的值 ,那么这个函数的收敛性越好。
(同理,收敛就是趋近的意思)
级数:将无穷 多个数按照一定顺序相加的表达式 ,形式为:
前导(最重要的三个问题)
在我们的计算中,常常会遇见类似这样的数列(斐波那契):
对这样一个数列,我们想知道以下三个问题:
(1)某项的值。
(2)定义正整数 ,把数列
到
项的值看作
种物体各种对应的数量,
求取 个物体时的方案数(组合数)。
(3)定义正整数 ,把数列
到
项的值看作
种物体各种对应的数量,
且每种 物体都有各自的编号(每种互相不同,一种里面是相同的),
求取 个物体时的不同 的方案数(排列数)。
怎么样,略有难度是不是?
这是因为本质上数列是数的集合 ,它们内部是"离散"的。
上面的问题你也许可以通过找规律 来解决,但大多时候数列是没有明显规律的。
这时生成函数就派上用场了!它可以快速的解决上述问题。
1.普通生成函数的定义及公式推导
假设一个这样的数列:
它的普通生成函数 的幂级数展开式定义:
也就是:
是不是有点蒙?继续往下看。
举个例子,之前提到的斐波那契数列 ,它的普通生成函数 (幂级数展开式)是:
我们不在意这个函数的值,而关注它每项的系数。
聪明的同学肯定发现了, 的
次项的系数 就是数列第
项的值。
这个生成函数,还有另一种写法(闭式 ):。
(闭式的意思就是直接和 相关)
这两个公式看着完全不一样,它们是怎样互相转换的呢?
先从幂级数展开式 到闭式 
假设我们已经知道了神秘的通项公式(这个只能发挥你的聪明才智):
然后要把这个递推式变成与整个数列相关的函数,
设 ,为了消掉
的高次项 ,考虑构造阶梯式。
(说白了就是造几个函数,使得它们这样列在一起 相减 or 加能把同一 项消掉)
我们发现 的式子右边就只剩下
了。
已知 ,则有:
搞定。
然后是闭式 推导到幂级数展开式 
设
两边同乘 :
把式子右边的展开来,每一项 的对应系数如下:
|--------------------------------------|--------------------------------|
| (常数项) |
|
| |
|
| |
|
| |
|
| |
|
| |
|
(怎么得出这个表的呢?就拿 来说。它的系数可以来自
、
和
,把常数提出来就是
)
又因为式子右边的值为 ,所以除了
要等于
以外,其他系数要全部为
。
那我们就有了通项公式:
有了这玩意想求哪个求哪个,顺便把我们提出的第一个问题解决了。
至于如何求出数列?求出每一项的系数带进去就好。
(篇幅有限,只演示了斐波那契数列两种生成函数写法的互相推导。
其他的数列也是一样的,知道递推式就能推出闭式表达式,
知道闭式表达式就能推出递推式,进而推出幂级数展开式)
总结:
数列:
普通生成函数:
(适用于多重集组合数问题)
幂级数展开式:
闭式:
幂级数展开式 到闭式: 自己观察出递推式 ,然后构造阶梯式 ,让高次项系数 都变为 。
闭式 推导到幂级数展开式 :假设系数 ,两边同乘消掉分母 ,分析不同幂次 的 的系数,
总结出通项公式 ,进而求出完整的数列。
2.普通生成函数的卷积&例题
回到第二个问题:
定义正整数
,把数列
到
项的值看作
种物体各种对应的数量,
求取
个物体时的方案数(组合数)。
解决这种多重集组合数 问题,我们可以给每一种 物体构造普通生成函数,
再计算它们的乘积 ,最后答案取某一项的系数。
设有 种物体,各有
个,则第
种物体的普通生成函数为:
(比如说 就代表着取三个这种物体 ,系数
代表这是一种情况 ,累计
)
那么怎么把两个生成函数"合体"呢?
我们需要------
卷积: 两个函数作用产生第三个函数的过程。
普通生成函数的卷积 形式可以用 这个表示。
公式长这样:
(就是最后要保证这项 的次 为所需值(
),直接背就好啦,看不懂直接看代码)
所以我们要做的就是统计:
这坨东西中 的系数 ,即取
个物体时的方案数。
例题:水果的組合HDU2152
题意:有 种水果,每种水果选购个数在
之间,问买
个水果有多少种方案?
神秘代码:
cpp
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N=1100,M=2100;
int n, m, L[N], R[N];
ULL C[M], ans[M];
void calc(){
memset(C, 0, sizeof(C));
for(int i=L[1]; i<=R[1]; i++)C[i]=1;
for(int k=2; k<=n; k++){
memset(ans, 0, sizeof(ans));
for(int i=0; i<=m; i++)
for(int j=L[k]; j<=R[k] && i+j<=m; j++)
//当前取 j个,之前取 i个
ans[i+j]+=C[i];
memcpy(C,ans,sizeof(ans));
}
printf("%llu\n", C[m]);
}
int main(){
while(scanf("%d%d", &n, &m)!=EOF){
for(int i=1; i<=n; i++){
scanf("%d%d", &L[i], &R[i]);
R[i]=min(R[i], m);
}
calc();
}
return 0;
}
总结:
普通生成函数适用于多重集组合数问题,
比如:无标签组合选物品 (大家长得都一样),且信息都在多项式的系数里。
卷积 公式:
3.指数生成函数的定义及泰勒展开
先忘了指数生成函数,我们来看看------
泰勒展开。
(因为本解析面对的人群可能包含中小学生,我这边再解释下,会的可以通过目录跳过这段。)
(初学者请不要跳过!!这个知识点十分重要,之后推导闭式还会用到!!)
想象一下,你要画一条弯曲的曲线 ,但你只有直尺和铅笔。
那我们就要像祖冲之一样,把曲线切割成很多段线段。
泰勒展开 就是这样,在某一个点,我们尽最大努力贴近 这条曲线这个点周围的一段。
有聪明的同学会想到求导 ,因为导数的几何意义就是曲线上该点切线,只要给这个点求个导就好。
但其实自己实际试一下,就会发现这样只求一次导,误差很大。
就好比你去旅游 只算直线距离 所需的时间,不考虑陡坡弯道和交通堵塞,时间当然就不对了。
解决方法也简单,多求几个导就行,求的导总阶数越大,整体的线就越贴近。
(可以理解为一阶导只是求斜率,二阶导能知道曲线的趋势是上升或者下降,导越多数据越详细)
我们请出神秘的泰勒展开公式:
(点 可以理解为固定点 ,
才是真正带进去的值。在
点,我们通过泰勒展开贴近
函数
点周围的一段 ,使得
点临近的
代入泰勒展开式 得到的结果与
近似)
难懂。把它展开看看。
彳亍,还是看不懂,这个阶乘是哪里来的,减 又是什么?
再来回顾一下泰勒展开的定义 :在已知函数在某一点的各阶导数值 的情况之下,泰勒公式可以用这些导数值做系数 构建一个多项式 来近似函数在这一点的邻域中的值。
(简单来说就是知道某个点 代入一个函数的各阶导 ,就能求出一个式子。当代入该式的点越接近点
,得出的值也与原函数 的值越接近)
是不是稍微有点头绪了?我们再来严格推导下。
我们设泰勒展开公式为:
(为什么是这个形式?先这样粗略理解下:因为 ,
这样无论求几阶导都有一项是没有 的,而那一项就等于原函数的值)
然后分别求 阶导,计算每项的系数。
求""阶时:
求一阶导时:
求二阶导时:
求三阶导时:
....
这样求出每一项的系数,就有了整个泰勒展开公式。
顺便提一嘴,麦克劳林展开 就是在 时的泰勒展开。
一般不说 是几直接求泰勒展开时,就是指麦克劳林展开。
我们经常用这玩意,因为大多数函数在 处方便求导,计算简单。
麦克劳林展开式:
聪明的同学们,让我们一起来试试麦克劳林展开 吧(^L^)
我们知道 求导后就是自身,所以也就有:
很简单吧^^
有同学要问了:讲了这么多泰勒展开,但我们不是要学指数生成函数吗?
先别急,清理掉脑子里的内存,看看指数生成函数。
by the way,虽然叫指数生成函数 ,但它和指数函数没太大关系。
式子长这样:
怎么样,是不是和普通生成函数 的式子很像 ,就只多了个除以的阶乘。
那究竟为什么要除以阶乘呢?
你还记得我们的第三个问题吗?
定义正整数
,把数列
到
项的值看作
种物体各种对应的数量,
且每种 物体都有各自的编号(每种互相不同,一种里面相同),
求取
个物体时的不同 的方案数(排列数)。
前文说过,普通 生成函数可以解决多重集组合数的问题,
而指数 生成函数在多重集排列集 问题上比前者更简便。
(PS:一个数列是既可以写成普通生成函数 又可以写成指数生成函数的)
举个例子,就拿阶乘数 来说,数列长这样:
普通生成函数长这样;
这东西系数到后面分散到无穷 ,收敛性极差(系数越来越大,函数没有确定的趋近值),
递推式 带个 ,自然也不能推导出简练的闭式。
根据前面的公式,指数生成函数长这样:
这个东西的系数递推式 非常明显,。
(系数都一样为 了还想咋滴)
太好了,可以推导闭式表达式!
设 ,
老样子,上减下,且:
嘿嘿,这公式真好看。
有同学可能会问了:这不对啊!主播你只是刚刚好选了个**阶乘数 **的数列,
怎么就能说明指数 生成函数在多重集排列集 问题上比普通 生成函数更简便呢?
哎,你还真说对了。但是想下:选 个物体,其中有
个物体是相同的(其它都不同)。
那么答案就是 。很明显,和阶乘有很大关系。
所以在这种多重集排列集 问题中,阶乘 经常用到,自然指数生成函数 也就更简便了。
但是指数生成函数又和泰勒展开有什么关系呢?
(这一块只做拓展,不想看可以不看)
上面推导过,这是 的麦克劳林展开:
我们发现,这正好就是序列 的指数生成函数。
实际上,对比两个公式(上指下麦):
即,序列 中的 决定了函数
在
处的所有导数。
泰勒展开可以把离散的数列 直接转换成解析函数(闭式表达式)。
(至于怎么构造函数那是大学的事情了pwp,大家记得泰勒展开的基本形式就行了)
总结:
泰勒展开式:
(点 为固定点 ,
是带进去的值。
在 点,我们通过泰勒展开贴近
函数
点周围的一段,
使得 点临近的
代入泰勒展开式 得到的结果与
近似)
麦克劳林展开式:
(在 时的泰勒展开)
指数生成函数:
(比普通生成函数 多个除以的阶乘,解决多重集排列数的问题)
指数生成函数 和泰勒展开 的形式是完全一样的,推导闭式 的逆过程 就是泰勒展开。
4.指数生成函数的卷积&例题
就像普通生成函数 一样,指数生成函数也有卷积。
公式长这样:
我们要求:
这坨东西中 的系数。
设 ,且
。
那么对于每一种满足条件的 组合,我们都会得到一个项
。
最终的答案是对所有这些可能的组合求和:
所有可能的项
从中提出 得到系数:
。
答案就为 。
例题:水果的排列HDU1521
题意:有 种水果,每种水果有
个,问取
个水果的排列数。
神秘代码:
cpp
#include<bits/stdc++.h>
using namespace std;
int n, m, a[11];
double fac[11], C[11], ans[11];
void calc(int m){
fac[0]=1.0; for(int i=1;i<=m;i++) fac[i]=fac[i-1]*i; //阶乘
memset(C, 0, sizeof(C));
for(int i=0; i<=a[1]; i++) C[i]=1.0/fac[i]; //初始系数
for(int k=2; k<=n; k++){
memset(ans, 0, sizeof(ans));
for(int i=0; i<=m; i++)
for(int j=0; j<=a[k] && i+j<=m; j++)
ans[i+j]+=C[i]/fac[j];
memcpy(C, ans, sizeof(ans));
}
printf("%.0lf\n", C[m]*fac[m]); //最后还要 *m!
}
int main(){
while(scanf("%d%d", &n, &m)!=EOF){
for(int i=1; i<=n; i++) scanf("%d", &a[i]);
calc(m);
}
return 0;
}
省流总结:
指数生成函数卷积和普通生成函数卷积长一样就多了个组合数 。
5.生成函数的实际运用公式
最终boss来了哦!让我们一步步撕开它神秘的面具!
part 1:二项式定理
就算你没上过高中,但只要学过组合数,那么一眼就能看出:
(对于任意正整数 )
然而这个式子,经过牛爵爷一通操作后成了------
广义二项式定理。
即,对于任意实数 (或复数),以及满足
的
,有:
你可能会说:老师,这和上面那个二项式定理长的不是一毛一样吗为什么还要推导
呵呵你还真说对了
可是它们 的取值域 不一样,二项式定理 只能在
为正整数的时候起效,
而广义二项式定理 适用于任意实数 。
嘛,来推导,用泰勒级数展开。
考虑函数 ,在
处展开为泰勒级数。
麦克劳林展开:
求多阶导数:
代入 :
(为啥要求 ,因为泰勒公式 可以近似 函数在这一点的邻域中的值,
超过这个固定范围 得出的值就可能错误,这就是收敛半径,挖个坑我以后讲怎么推导收敛半径)
泰勒展开推导保证了,无论 是多少,都有
。
(题外:牛顿的这一发现标志着数学从有限向无限的重要跨越,为后续的分析学发展奠定了基础)
广义二项式展开(指数为负数的特殊情况推导)
推导过程:
根据上面,则有:
取相反数:
(为什么要这么干?因为指数生成函数经常用到 ,有了这玩意方便,具体的见下文)
part 2:无限级数的普通生成函数公式
嘛,人生在世,难免需要自己推导闭式表达式。
但一个个的太不现实了,有没有通用的公式呢?
先来看几个常用的例子(先不要探究为什么,先找规律):
(1)常数数列(全部系数都一样)
这个一眼就看得出来,
推导也简单:
(2)自然数数列
直接推导吧,我们都知道:
这个式子两边同时求导:
(为啥可以同时导?
通常只要恒等式 两边的函数满足定义域相同 且函数都可导,那么就可以放心的导俩函数。
但这里还要讨论收敛半径 的问题,如果 的取值在某个范围 内且无穷级数收敛
(有确定的和),那么这个范围就是级数的收敛半径。
比如说这里的收敛半径 就是 ,只要
在这个范围内,
级数就能收敛 为一个可导的函数)
神奇的发现右边只要再乘个 就是自然数数列的生成函数!
(以后遇到像 这样的数列,就直接让闭式乘对应的倍数就好!)
(3)平方数列
细心的同学肯定看出来了!其实就是把上面的自然数数列再导一遍,这里就不解释了。
(4)二项式系数数列 (三角数列)
(就是杨辉三角有一斜的那些数)
注意到这些数都是 ,
爵爷的广义二项式展开告诉我们:
嘛,改一下:
当 时,有:
可是我们想要 而不是
,不过没关系:
设 ,则有:
(这里悄悄改了下定义域,不过当 时那些项也为
,所以没事)
(5)一般的二项式数列
根据前面三角数列的推导,你觉得好像是:
(想想也是嘛 多大就得提出来多少个
)
严格的证明(数学归纳法):
(其实没啥必要,看懂记住上面三角数列,这个也是一样的)
当 时:
其他情况我们假设对 成立:
那么只要证明对 成立:
从这个式子开始:
老样子,先求导,这样才能使右边式子出现 。
好了,忘掉上面那坨,我们来看一个小学难度的组合数引理:
看起来好像没活了,其实不然。
等式右边前面那坨可以上下同乘 ,这样分母 就相同了:
perfect!
还记得前面这坨吗:
两边同乘 :
等式左边这个形式简直和引理 一毛一样!!(把 看作
)
等式右边 后面那坨看着眼熟,不就是
ok,还挺巧的,那么就有:
移移移:
可以可以,两边同除以 :
终于等到你~~~~
(6)递推式数列
in fact,有时数列的系数 不一定是二项式系数。
just like 斐波那契数列 ,就需要我们自己整理出递推式。
具体的前面在普通生成函数的定义及公式推导 那里就已经细致推过了,我们在总结下步骤。
第一:找出递推式。
第二:定义生成函数 。
第三:根据递推式 造几个函数,使得它们列在一起 相减 or 加能把同一 项消掉。
第四:列出计算式,提出 ,整理出闭式。
总结这一 part(最重要的三个公式+特殊情况):
递推式需要多练习!
part 3:有限数列的生成函数公式
呵呵,你以为推导完无限的就完了吗?
有限数列也是题目中的常客!
(1)有限等比数列:
直接上公式:
长得挺别致,看看怎么推导的:
设 ,
上减下,则有:
其实也不是很难嘛^^
(2)有限自然数数列:
(这个不咋用了解一下得了)
如果前面你已经认真看过,那么你就知道应该怎么做:
两边求个导,化简完乘 。
老样子,还是从等比数列开始:
等式右边 就是:
左边:
两边乘以 :
有点难看,将就着吧。
(3)有限二项式系数数列:
这玩意,跟之前学过的无限一般二项式数列 不一样,现在学的式子括号里上标是固定 的,前面学的式子下标是固定的。
我猜你一定不想再推个公式了,所以我们请出二项式定理!
直接代入 ,得出:
(特别好牛爵爷让我少推一个公式🥹)
总结这一 part(最重要的两个公式):
(为什么公式这么少?因为之前无限级数的公式一环套一环,而且都很简洁,
这里只背两个经常用的公式就好)
part 4:无限级数指数生成函数的闭式(泰勒展开式)
我们先来复习一遍麦克劳林展开式 :
我们想求 ,
而之前我们求过 的麦克劳林展开:
所以得出:
次为奇数的项变号,就有:
两个数列加 起来,就能得到系数为偶数 的级数:
同理相减 ,得到系数为奇数 的级数:
(关于有限数列的指数生成函数,那个超纲了也不经常用,我先挖个坑^^)
6.生成函数应用三道例题
先看题自己尝试写一下各种物体的生成函数,再看解析。
后面两题:看这个老师的视频!讲得特别好!
洛谷 P6078 CEOI 2004 Sweets
P10780 BZOJ3028 食物
代码:
cpp
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const LL P=10007;
char s[510];
LL qpow(LL a, LL b){
int res=1;
while(b){
if(b&1) res=res*a%P;
a=a*a%P; b/=2;
}
return res;
}
int main(){
scanf("%s", s); //这 n老大了,只能这么读
LL n=0, len=strlen(s);
for(int i=0; i<len; i++) n=(n*10%P+s[i]-'0')%P;
LL ans=n*(n+1)%P *(n+2)%P *qpow(6ll,P-2)%P; //逆元,不懂去我主页找
printf("%lld\n", ans);
return 0;
}
POJ3734 Blocks
代码:
cpp
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const LL P=10007;
LL qpow(LL a, LL b){
int res=1;
while(b){
if(b&1) res=res*a%P;
a=a*a%P; b/=2;
}
return res;
}
int main(){
int T; scanf("%d", &T);
while(T--){
LL n; scanf("%lld", &n);
LL ans=(qpow(4ll,n) +qpow(2ll,n+1))%P *qpow(4ll,P-2)%P;
printf("%lld\n", ans);
}
return 0;
}
参考资料(鸣谢):
update 2025.8.19:更改了一些句子的格式,使其更美观。
修正了第五部分生成函数的实际运用公式里的三个错误。
update 2025.12.14:修正了两个错误,感谢 @2501_94561993 的指出。
secondary publication 2025.9.16
待办:有限数列的指数生成函数,收敛半径详细推导。