通过斐波那契数列,我们掌握了一些基本的生成函数使用技巧。
现在我们来了解一些常见的数列的生成函数。
常数数列
首先我们来看最简单的常数数列。比如,全是1,表示如下:
1,1,1,...,1,...1,1,1,...,1,...1,1,1,...,1,...
它的生成函数可以用类似斐波那契的思路,错项相减得到:
G(x)=1+x+x2+...+xn+...=11−xG(x) = 1 + x + x^2 + ...+x^n+... = \frac{1}{1-x}G(x)=1+x+x2+...+xn+...=1−x1
推广到任意常数k的情况,则可以给生成函数整体乘以常数k。
数列:k,k,...,k,...数列:k,k,...,k,...数列:k,k,...,k,...
G(x)=k+kx+kx2+...+kxn+...=k1−xG(x) = k + kx + kx^2 + ...+kx^n+... = \frac{k}{1-x}G(x)=k+kx+kx2+...+kxn+...=1−xk
生成函数的移位和填项
有时我们定义数列,它从某一项开始才会遵循特定规律,这种时候我们可以先给生成函数乘以x的某次幂,进行移位。例如,某数列前三位是0,后面是常数1,那么,我们就给全是1的数列的生成函数乘以x3x^3x3
0,0,0,1,1,1,...,1,...0,0,0,1,1,1,...,1,...0,0,0,1,1,1,...,1,...
G(x)=x3+x4+...+xn+...=x31−xG(x) = x^3 + x^4 + ...+x^n+... = \frac{x^3}{1-x}G(x)=x3+x4+...+xn+...=1−xx3
如果前三位不希望是0,而是1,2,3,则可以把对应的多项式加到原来的生成函数上:
1,2,3,1,1,1,...,1,...1,2,3,1,1,1,...,1,...1,2,3,1,1,1,...,1,...
G(x)=1+2x+3x2+x3+x4+...+xn+...=1+2x+3x2+x31−xG(x) = 1 + 2x + 3x^2 + x^3 + x^4 + ...+x^n+... = 1 + 2x + 3x^2 + \frac{x^3}{1-x}G(x)=1+2x+3x2+x3+x4+...+xn+...=1+2x+3x2+1−xx3
自然数列
自然数列:
1,2,3,...,n,...1,2,3,...,n,...1,2,3,...,n,...
自然数列的处理思路比较特别,我们需要利用导数公式,把常数数列移位后求导。
我们约定G_c(x)G\_c(x)G_c(x) 是常数数列 1,1,1,...,1,...{1,1,1,...,1,...}1,1,1,...,1,...的生成函数
Gc(x)=1+x+x2+...+xn+...G(x)=ddx(xGc(x))=1+2x+3x2+...+nxn+...G_c(x) = 1 + x + x^2 + ...+x^n+... \\
G(x) = \frac{d}{dx}(xG_c(x)) = 1 + 2x + 3x^2 + ...+nx^n+... \\Gc(x)=1+x+x2+...+xn+...G(x)=dxd(xGc(x))=1+2x+3x2+...+nxn+...
考虑到我们已知
Gc(x)=11−xG_c(x) = \frac{1}{1-x}Gc(x)=1−x1
把它代入到G(x)
G(x)=ddx(x1−x)=1(1−x)2G(x) = \frac{d}{dx}(\frac{x}{1-x}) = \frac{1}{(1-x)^2}G(x)=dxd(1−xx)=(1−x)21
可以在wolframalpha中查看求导的步骤以及验证最后展开的形式。
平方数列
平方数列:
12,22,32,...,n2,...1^2,2^2,3^2,...,n^2,...12,22,32,...,n2,...
参考从常数数列到自然数列的思路,我们把自然数列的生成函数乘以x,再求导即可:
G(x)=ddx(x(1−x)2)=x+1(1−x)3G(x) = \frac{d}{dx}(\frac{x}{(1-x)^2}) = \frac{x+1}{(1-x)^3}G(x)=dxd((1−x)2x)=(1−x)3x+1
在wolframalpha中查看
任意次方数列
作为平方数列的推广,我们可以推导任意次方数列的生成函数
1k,2k,3k,...,nk,...1^k,2^k,3^k,...,n^k,...1k,2k,3k,...,nk,...
Gk(x)=1+2kx+3kx2+...+nkxn+...G_k(x) = 1 + 2^kx + 3^kx^2 + ...+n^kx^n+...Gk(x)=1+2kx+3kx2+...+nkxn+...
这个生成函数没有一般的初等代数形式,但是我们可以从低阶的生成函数递推得到:
Gk(x)=xGk−1′(x)G_k(x) = xG_{k-1}'(x)Gk(x)=xGk−1′(x)
例如,三次方数列:
G3(x)=xG2′(x)=ddxx(x+1)(1−x)3=x2+4x+1(1−x)4G_3(x) = xG_2'(x) = \frac{d}{dx}\frac{x(x+1)}{(1-x)^3}=\frac{x^2+4x+1}{(1-x)^4}G3(x)=xG2′(x)=dxd(1−x)3x(x+1)=(1−x)4x2+4x+1
在wolframalpha中查看
数列相加
我们考虑两个数列
a0,a1,a2,a3,...,an,...b0,b1,b2,b3,...,bn,...a_0,a_1,a_2,a_3,...,a_n,... \\
b_0,b_1,b_2,b_3,...,b_n,... a0,a1,a2,a3,...,an,...b0,b1,b2,b3,...,bn,...
现在假设我们要求一个数列:
a0+b0,a1+b1,a2+b2,a3+b3,...,an+bn,...a_0+b_0,a_1+b_1,a_2+b_2,a_3+b_3,...,a_n+b_n,... \\a0+b0,a1+b1,a2+b2,a3+b3,...,an+bn,...
那么,根据多项式运算的性质,这个新数列的生成函数显然等于,此二数列生成函数相加:
G(x)=Ga(x)+Gb(x)G(x) = G_a(x) + G_b(x)G(x)=Ga(x)+Gb(x)
多项式数列
有了数列相加和任意次方相关的知识,我们来考虑多项式数列:
an=c0+c1n+c2n2+...+cknka_n =c_0+c_1n+c_2n^2+...+c_kn^kan=c0+c1n+c2n2+...+cknk
我们可以把它按每一项分解后,对生成函数求和:
G(x)=c011−x+c11(1−x)2+c2x+1(1−x)3+c3x2+4x+1(1−x)4+......G(x) = c_0\frac{1}{1-x} + c_1\frac{1}{(1-x)^2} + c_2\frac{x+1}{(1-x)^3} + c_3\frac{x^2+4x+1}{(1-x)^4} + ......G(x)=c01−x1+c1(1−x)21+c2(1−x)3x+1+c3(1−x)4x2+4x+1+......
求前n项和
通过生成函数,我们可以解决任意数列的前n项和问题。
原数列
a0,a1,a2,a3,...,an,...a_0,a_1,a_2,a_3,...,a_n,... \\a0,a1,a2,a3,...,an,...
我们假设其前n项和数列为
0,s1,s2,s3,...,sn,...0,s_1,s_2,s_3,...,s_n,... \\0,s1,s2,s3,...,sn,...
那么根据前n项和的定义,有:
sn+1=sn+ans_{n+1}=s_n + a_nsn+1=sn+an
我们假设原数列的生成函数是G(x)G(x)G(x),前n项和数列的生成函数是Gs(x)G_s(x)Gs(x)
Gs(x)=xGs(x)+xG(x)G_s(x) = x G_s(x) + xG(x)\\Gs(x)=xGs(x)+xG(x)
变换后可以得到:
Gs(x)=x1−xG(x)G_s(x) = \frac{x}{1-x}G(x)Gs(x)=1−xxG(x)
例如,我们要求平方数列的前n项和
12,22,32,...,n2,...1^2,2^2,3^2,...,n^2,...12,22,32,...,n2,...
根据前面推理:
Gs(x)=x1−xG(x)=x1−x⋅x+1(1−x)3=x2+x(1−x)4G_s(x) = \frac{x}{1-x}G(x) \\
= \frac{x}{1-x} \cdot \frac{x+1}{(1-x)^3} \\
= \frac{x^2+x}{(1-x)^4} \\Gs(x)=1−xxG(x)=1−xx⋅(1−x)3x+1=(1−x)4x2+x
直接泰勒级数展开可得:
sn=16n(1+3n+2n2)s_n = \frac16{n(1+3n+2n^2)}sn=61n(1+3n+2n2)
在wolframalpha中查看
组合数
从n个元素中选出k个元素的方案数。
Cnk=n(n−1)(n−2)...(n−k)1×2×3×...×k=n!(n−k)!k!
C_n^k = \frac{n(n - 1)(n - 2)...(n-k)}{1 \times 2\times 3\times ...\times k} = \frac{n!}{(n-k)!k!}
Cnk=1×2×3×...×kn(n−1)(n−2)...(n−k)=(n−k)!k!n!
G_k(x)=1(1−x)k
G\_k(x) = \frac{1}{(1-x)^k}
G_k(x)=(1−x)k1
当我们反过来,认为n固定,数列中第k项是CnkC_n^kCnk时, 数列变成了:
Cn1, Cn2, ...Cnk,...CnnC_n^1,\ C_n^2,\ ...C_n^k,...C_n^n\\Cn1, Cn2, ...Cnk,...Cnn
此时,数列生成函数为:
G(x)=(1+x)nG(x) = (1+x)^nG(x)=(1+x)n
这个形式是著名的二项式定理,也是杨辉三角形第n行第k个数。
11 11 2 1 1 3 3 11 4 6 4 1......Cn1 Cn2 ...Cnk...Cnn......1\\
1\ 1\\
1\ 2\ 1\ \\
1\ 3\ 3\ 1 \\
1\ 4\ 6\ 4\ 1 \\
... ...\\
C_n^1\ C_n^2\ ...C_n^k...C_n^n\\
... ...\\
11 11 2 1 1 3 3 11 4 6 4 1......Cn1 Cn2 ...Cnk...Cnn......
生成函数的组合意义
当我们从两个不同集合中,选取总数为n的组合时,假设两个池子中可以选取的数量的生成函数分别是G1(x)G_1(x)G1(x)和G2(x)G_2(x)G2(x),那么组合数G(x)的生成函数为二者相乘:
G(x)=G1(x)⋅G2(x)G(x) = G_1(x) \cdot G_2(x)G(x)=G1(x)⋅G2(x)
我们在第一节中苹果、香蕉、梨的组合方式就是例子。
生成函数的递推意义
有1分、2分、5分硬币若干,想要凑出1元钱,求有多少种凑法。
G(x)=(1+x1+x2+x3+...+...)(1+x2+x4+x6+...+...)(1+x5+x25+x125+...+...)
G(x)=(1+x^{1}+x^{2}+x^{3}+...+...)(1+x^{2}+x^{4}+x^{6}+...+...)(1+x^{5}+x^{25}+x^{125}+...+...)
G(x)=(1+x1+x2+x3+...+...)(1+x2+x4+x6+...+...)(1+x5+x25+x125+...+...)
G(x)=1(1−x)(1−x2)(1−x5)
G(x)=\frac{1}{(1-x)(1-x^2)(1-x^5)}
G(x)=(1−x)(1−x2)(1−x5)1
但是这个函数次数略高,wolframalpha也不好解了。
首先我们把它展开:
G(x)=1(1−x)(1−x2)(1−x5)=11−x−x2+x3−x5+x6+x7−x8
G(x)=\frac{1}{(1-x)(1-x^2)(1-x^5)} = \frac{1}{1 - x - x^2 + x^3 - x^5 + x^6 + x^7 - x^8}G(x)=(1−x)(1−x2)(1−x5)1=1−x−x2+x3−x5+x6+x7−x81
我们再回忆一下,什么样的递推关系能够产生这个形状的生成函数呢?我们把斐波那契数列的情况推广一下,假设有这样一个递推关系:
Fn+k=a0Fn+a1Fn+1+a2Fn+2+...+ak−1Fn+k−1F_{n+k} = a_0F_n+a_1F_{n+1} +a_2F_{n+2}+...+a_{k - 1}F_{n + k - 1}Fn+k=a0Fn+a1Fn+1+a2Fn+2+...+ak−1Fn+k−1
它的生成函数是:
G(x)=11−ak−1x+ak−2x2+...+a1xk−1
G(x) = \frac{1}{1 - a_{k-1}x + a_{k-2}x^{2} + ... + a_1x^{k - 1}}
G(x)=1−ak−1x+ak−2x2+...+a1xk−11
那么,我们反过来,也可以从这个形状的生成函数,得到递推关系:
an+8=an+7+an+6−an+5+an+3−an+2−an+1+ana_{n + 8} = a_{n+7} + a_{n+6} - a_{n+5} + a_{n+3} - a_{n+2} - a_{n+1} + a_nan+8=an+7+an+6−an+5+an+3−an+2−an+1+an
既然有了递推关系,我们又可以使用矩阵快速幂啦!
上一篇中,我们编写了一个二维矩阵的乘法,我们首先把它扩展成一个通用矩阵乘法:
const mul = (m1, m2) =>
m1.map((row) => m2.map((any, j) => row.map((v, i) => v * m2[i][j]).reduce((a, b) => a + b, 0)));
快速幂沿用之前的代码
const pow = (v, n, mul = (a, b) => a * b) => {
let r = v, m = n - 1, c = v;
while(m > 0) {
if(m % 2)
r = mul(r, c);
m = m >> 1;
c = mul(c, c);
}
return r;
}
之后我们搞一个传输矩阵和原始矩阵,就可以算出结果啦:
const transformMatrix = [
[0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, -1],
[0, 1, 0, 0, 0, 0, 0, -1],
[0, 0, 1, 0, 0, 0, 0, 1],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, -1],
[0, 0, 0, 0, 0, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 1, 1]]
const originMatrix = [
[1, 1, 2, 2, 3, 4, 5, 6],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
]
const countCoin = n => mul(originMatrix, pow(transformMatrix, n, mul))[0][0];
console.log(countCoin(100)); //541
在关于硬币的几个动态规划问题中,我已经给出了此题的动态规划解法。我们这里刚好可以拿来验证。
我们把它推广到任意种类硬币的情况:
给定若干种数量不限的硬币的币值,编写代码计算n分有几种表示法。
设硬币面值为:
v1,v2,...,vkv_1, v_2, ..., v_kv1,v2,...,vk
生成函数为:
G(x)=∏i=1k(1+xvi+(xvi)2+(xvi)3+...)=1∏i=1k(1−xvi)G(x)=\prod_{i=1}^{k}(1+x^{v_i}+(x^{v_i})^2+(x^{v_i})^3+...)
=\frac{1}{\prod_{i=1}^{k}(1-x^{v_i})}G(x)=i=1∏k(1+xvi+(xvi)2+(xvi)3+...)=∏i=1k(1−xvi)1
我们要先编写多项式乘法代码来计算它的递推函数:
const polynomialMultiple = (p1, p2) => {
let r = Array(p1.length + p2.length - 1).fill(0);
p1.forEach((v1, i) =>
p2.forEach((v2, j) => r[i + j] += v1 * v2)
);
return r;
}
注意,这个多项式相乘并不是经过优化的算法,仅仅是字面解法,我们在后文中会介绍多项式乘法的优化算法。
最后我们用代码生成传输矩阵和原始矩阵,就可以使用快速幂来求最终结果了:
function countCoins(values, n) {
const transformMatrix = values
.map(v => [1].concat(Array(v - 1).fill(0)).concat([-1]))
.reduce(polynomialMultiple)
.slice(1)
.map(v => -v)
.reverse()
.map((v, i, {length}) => {
let r = Array(length - 1).fill(0);
(i > 0) && (r[i - 1] = 1);
return r.concat(v);
})
let r = [1].concat(Array(transformMatrix.length - 1).fill(0));
for(let value of values)
for(let i = value; i < transformMatrix.length; i++)
r[i] += r[i - value];
const originMatrix = [r].concat(
Array(transformMatrix.length - 1).fill(Array(transformMatrix.length).fill(0)))
return mul(originMatrix, pow(transformMatrix, n, mul))[0][0];
}
countCoins([1,2,5],100); //541
经过验证,结果与快速幂的结果一致,但是时间复杂度从O(n)O(n)O(n)降低到了O(log(n))O(log(n))O(log(n))。
针对本题,实际上还有时间复杂度更好的解法,我们留待后文讨论。
其它一些数列
等差数列
a0,a1,a2,a3,...,an,...其中,an+1−an=da_0,a_1,a_2,a_3,...,a_n,... \\
其中, a_{n+1} - a_{n} = da0,a1,a2,a3,...,an,...其中,an+1−an=d
G(x)=d(1−x)2+a_0(1−x)
G(x)=\frac{d}{(1-x)^2} + \frac{a\_0}{(1-x)}
G(x)=(1−x)2d+(1−x)a_0
等比数列
a0,a1,a2,a3,...,an,...其中,an+1an=c
a_0,a_1,a_2,a_3,...,a_n,...\\
其中, \frac{a_{n+1}}{a_{n}} = c
a0,a1,a2,a3,...,an,...其中,anan+1=c
G(x)=a0(1−cx)
G(x)=\frac{a_0}{(1-cx)}
G(x)=(1−cx)a0
循环数列
1,2,3,1,2,3,...,1,2,3...1,2,3,1,2,3,...,1,2,3...1,2,3,1,2,3,...,1,2,3...
G(x)=1+2x+3x2+...+(n%3+1)xn+...=1+2x+3x31−x3G(x) = 1 + 2x + 3x^2 + ...+(n\%3+1)x^n+... = \frac{1+2x+3x^3}{1-x^3}G(x)=1+2x+3x2+...+(n%3+1)xn+...=1−x31+2x+3x3
小结
此处推荐一个数学公益网站:oeis.org/ 这里收录了大量的数列,提供它们的生成函数、递推公式和相关论文,本文中介绍的基本数列都在其中。
针对生成函数的初等代数变换可以极大地覆盖组合问题,我们遇到题目如有以下特点,可以往这个方向思考:
- 求解结果是单个数值,且较大
- 题目规模n有可能存在递推关系,或者组合关系