【代数与组合数学 | 那忘算 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

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

相关推荐
Zane19941 小时前
快速排序凭什么叫"快"排序?平均O(nlogn)背后,藏着一个能让它退化成O(n²)的选择
算法
无忧.芙桃1 小时前
数据结构之排序算法(上):从评价指标到插入、希尔、选择与堆排序
c语言·c++·排序算法
syagain_zsx1 小时前
算法基础篇 · 02 高精度(C++ 题解)
开发语言·c++·学习·高精度
6Hzlia1 小时前
【Classic 150 刷题计划】 LeetCode 242. 有效的字母异位词 | C++ 哈希计数与严密防线
c++·算法·leetcode
wabs6661 小时前
关于二叉树【力扣101.对称二叉树的思考】
数据结构·c++·算法·leetcode·二叉树
6Hzlia1 小时前
【Classic 150 刷题计划】 LeetCode 228. 汇总区间 | C++ 双游标区间扫描与 to_string 规范
c++·算法·leetcode
玖玥拾2 小时前
LeetCode 383 赎金信
算法·leetcode
维克兜率天2 小时前
5.3 宏观因子:抬头看天
笔记·python·深度学习·算法·量化
LuminousCPP2 小时前
数据结构-排序(五):计数排序与排序专题阶段总结|从外部归并到线性排序,再看算法边界与选型
c语言·数据结构·笔记·算法·排序算法