likes
comments
collection
share

降维打击面试题系列:生成函数(三)常见数列与生成函数

作者站长头像
站长
· 阅读数 15

通过斐波那契数列,我们掌握了一些基本的生成函数使用技巧。

现在我们来了解一些常见的数列的生成函数。

常数数列

首先我们来看最简单的常数数列。比如,全是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+...=1x1

推广到任意常数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+...=1xk

生成函数的移位和填项

有时我们定义数列,它从某一项开始才会遵循特定规律,这种时候我们可以先给生成函数乘以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+...=1xx3

如果前三位不希望是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+1xx3

自然数列

自然数列:

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)=1x1

把它代入到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(1xx)=(1x)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((1x)2x)=(1x)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)=xGk1(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(1x)3x(x+1)=(1x)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)=c01x1+c1(1x)21+c2(1x)3x+1+c3(1x)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)=1xxG(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)=1xxG(x)=1xx(1x)3x+1=(1x)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(n1)(n2)...(nk)=(nk)!k!n!
G_k(x)=1(1−x)k G\_k(x) = \frac{1}{(1-x)^k} G_k(x)=(1x)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)=(1x)(1x2)(1x5)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)=(1x)(1x2)(1x5)1=1xx2+x3x5+x6+x7x81

我们再回忆一下,什么样的递推关系能够产生这个形状的生成函数呢?我们把斐波那契数列的情况推广一下,假设有这样一个递推关系:

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+...+ak1Fn+k1

它的生成函数是:

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)=1ak1x+ak2x2+...+a1xk11

那么,我们反过来,也可以从这个形状的生成函数,得到递推关系:

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+6an+5+an+3an+2an+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=1k(1+xvi+(xvi)2+(xvi)3+...)=i=1k(1xvi)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+1an=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)=(1x)2d+(1x)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)=(1cx)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+...=1x31+2x+3x3

小结

此处推荐一个数学公益网站:oeis.org/ 这里收录了大量的数列,提供它们的生成函数、递推公式和相关论文,本文中介绍的基本数列都在其中。

针对生成函数的初等代数变换可以极大地覆盖组合问题,我们遇到题目如有以下特点,可以往这个方向思考:

  1. 求解结果是单个数值,且较大
  2. 题目规模n有可能存在递推关系,或者组合关系