【代数与组合数学 | 那忘算 5】生成函数 & 例题 & 卷积

专栏:再来一遍一定记住的算法_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

这个题解在我写的这个博客里,为了方便学校OJ使用单开了一篇

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;
}

参考资料(鸣谢):

董晓算法

千问AI

一数

update 2025.8.19:更改了一些句子的格式,使其更美观。

修正了第五部分生成函数的实际运用公式里的三个错误。

update 2025.12.14:修正了两个错误,感谢 @2501_94561993 的指出。

secondary publication 2025.9.16

待办:有限数列的指数生成函数,收敛半径详细推导。

相关推荐
Nil2085 小时前
leetcode 118杨辉三角
算法·leetcode·职场和发展
NeilYuen7 小时前
【C++】STL源码仿写(二):vector
开发语言·c++
郝学胜_神的一滴7 小时前
游戏引擎原理与实践 04:拆解引擎基础系统与内存管理
c++·游戏
wanderist.7 小时前
线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数
java·数据结构·算法
All for pursuit.8 小时前
【设计-1】208.实现Trie (前缀树)
数据结构·c++·算法·leetcode
weixin_307779138 小时前
C++代码实现MATLAB中的crossval函数功能
开发语言·c++·算法·matlab
青少儿编程课堂8 小时前
背包问题(0/1 背包与完全背包)解题精讲——动态规划入门
c++·python·算法·bfs·信息学竞赛
倒头就睡的小比特8 小时前
C++多态
c++
光影少年8 小时前
为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?
前端·javascript·算法
HEJOO99 小时前
Linux 设备驱动系列——使用全局工作队列
java·linux·算法