母函数问题

系统 2106 0

今天大半天的时间在看这个。以下主要源于百度百科,讲得还是比较清楚。这里也可以看出百度百科和wiki的差别,wiki的公式都写得很漂亮,百度百科只是摘。

生成函数是说,构造这么一个多项式函数g(x),使得x的n次方系数为f(n)。 如:序列{0,1,2,3,4,5...n}的生成函数为:$f(x)=0+x+2x^2+3x^3+4x^4+...+nx^n$
生成函数最绝妙的是,某些生成函数可以化简为一个很简单的函数。也就是说,不一定每个生成函数都是用一长串多项式来表示的。比如,这个函数f(n)=1 (n当然是属于自然数的),它的生成函数就应该是$g(x)=1+x+x^2+x^3+x^4+\ldots$(每一项都是一,即使n=0时也有$x^0$系数为1,所以有常数项)。再仔细一看,这就是一个有无穷多项的等比数列求和嘛。如果-1<x<1,那么g(x)就等于1/(1-x)了。在研究生成函数时,我们都假设级数收敛, 因为生成函数的x没有实际意义,我们可以任意取值。 于是,我们就说,f(n)=1的生成函数是g(x)=1/(1-x)。


 

我们举一个例子说明,一些具有实际意义的组合问题也可以用像这样简单的一个函数全部表示出来。
考虑这个问题:从只有4个MM的二班选n个MM出来有多少种选法。学过简单的排列与组合的同学都知道,答案就是C(4,n)。也就是说。从n=0开始,问题的答案分别是1,4,6,4,1,0,0,0,...(从4个MM中选出4个以上的人来方案数当然为0喽)。那么它的生成函数g(x)就应该是$g(x)=1+4x+6x^2+4x^3+x^4$。这不就是……二项式展开吗?于是,$g(x)=(1+x)^4$。

你或许应该知道,$(1+x)^k=C(k,0)x^0+C(k,1)x^1+\ldots+C(k,k)x^k$;但你或许不知道,即使k为负数和小数的时候,也有类似的结论:$(1+x)^k=C(k,0)x^0+C(k,1)x^1+...+C(k,k)x^k+C(k,k+1)x^{k+1}+C(k,k+2)x^{k+2}+\ldots$(一直加到无穷)。其中,广义的组合数C(k,i)就等于k(k-1)(k-2)…(k-i+1)/i!,比如C(4,6)=4*3*2*1* 0 *(-1)/6!=0,再比如C(-1.4,2)=(-1.4)*(-2.4)/2!=1.68。后面这个就叫做 牛顿二项式定理 。当k为整数时,所有i>k时的C(k,i)中分子都要“越过”0这一项,因此后面C(k,k+1),C(k,k+2)之类的都为0了,与我们的经典二项式定理结论相同;不同的是,牛顿二项式定理中的指数k可以是任意实数。


 

我们再举一个例子说明一些更复杂的生成函数。n=x1+x2+x3+...+xk有多少个非负整数解?这道题是学排列与组合的经典例题了。把每组解的每个数都加1,就变成n+k=x1+x2+x3+...+xk的正整数解的个数了。

教材上或许会出现这么一个难听的名字叫“隔板法”:把n+k个东西排成一排,在n+k-1个空格中插入k-1个“隔板”。答案我们总是知道的,就是C(n+k-1,k-1)。它就等于C(n+k-1,n)。它关于n的生成函数是$g(x)=1/(1-x)^k$。这个生成函数是怎么来的呢?其实,它就是(1-x)的-k次方。把$(1-x)^{-k}$按照刚才的牛顿二项式展开,我们就得到了$x^n$的系数恰好是C(n+k-1,n),因为$C(-k,n)*(-x)^n=[(-1)^n*C(n+k-1,n)]*[(-1)^n*x^n]=C(n+k-1,n)x^n$。这里看晕了不要紧,后文有另一种方法可以推导出一模一样的公式。事实上,我们有一个纯组合数学的更简单的解释方法。因为我们刚才的几何级数$1+x+x^2+x^3+x^4+...=1/(1-x)$,那么$(1+x+x^2+x^3+x^4+...)^k$就等于$1/(1-x)^k$。仔细想想k个($1+x+x^2+x^3+x^4+\ldots$)相乘是什么意思。$(1+x+x^2+x^3+x^4+\ldots)^k$的展开式中,n次项的系数就是我们的答案,因为它的这个系数是由原式完全展开后k个指数加起来恰好等于n的项合并起来得到的。


 

现在我们引用《组合数学》上最经典的一个例题:
我们要从苹果、香蕉、橘子和梨中拿一些水果出来,要求苹果只能拿偶数个,香蕉的个数要是5的倍数,橘子最多拿4个,梨要么不拿,要么只能拿一个。问按这样的要求拿n个水果的方案数。
结合刚才的k个($1+x+x^2+x^3+x^4+\ldots$)相乘,我们也可以算出这个问题的生成函数。
$g(x)=(1+x^2+x^4+...)(1+x^5+x^10+..)(1+x+x^2+x^3+x^4)(1+x) \\
=[1/(1-x^2)]*[1/(1-x^5)]*[(1-x^5)/(1-x)]*(1+x) (前两个分别是公比为2和5的几何级数)\\
=1/(1-x)^2 \\
=(1-x)^{-2}=C(1,0)+C(2,1)x+C(3,2)x^2+C(4,3)x^3+\ldots (参见刚才对1/(1-x)^k的展开)\\
=1+2x+3x^2+4x^3+5x^4+....$

 于是,拿n个水果有n+1种方法。我们利用生成函数,完全使用代数手段得到了答案!


我们用两种方法得到了这样一个公式:$1/(1-x)^n=1+C(n,1)x^1+C(n+1,2)x^2+C(n+2,3)x^3+...+C(n+k-1,k)x^k+\ldots$。这个公式非常有用,是把一个生成函数还原为数列的武器。而且还是核武器。


接下来我们要演示如何使用生成函数求出Fibonacci数列的通项公式。
Fibonacci数列是这样一个递推数列:f(n)=f(n-1)+f(n-2)。现在我们需要求出它的生成函数g(x)。
$g(x)=x+x^2+2x^3+3x^4+5x^5+8x^6+13x^7\ldots$
等式两边同时乘以x,我们得到:
$x*g(x)=x^2+x^3+2x^4+3x^5+5x^6+8x^7+\ldots$
就像我们前面说过的一样,这相当于等式右边的所有系数向右移动了一位。
现在我们两式相加,我们得到:
$g(x)+x*g(x)=x+2x^2+3x^3+5x^4+8x^5+\ldots$
把这最后一个式子和第一个式子好好对比一下。如果第一个式子的系数往左边移动一位,然后把多余的“1”去掉,就变成了最后一个式子了。由于递推函数的性质,我们神奇地得到了:g(x)+x*g(x)=g(x)/x-1。也就是说,$g(x)*x^2+g(x)*x-g(x)=-x$。把左边的g(x)提出来,我们有:$g(x)(x^2+x-1)=-x$。于是,我们得到了$g(x)=x/(1-x-x^2)$。

现在把$x/(1-x-x^2)$还原成通项公式。这不是我们刚才的$1/(1-x)^n$的形式,我们要把它变成这种形式。

我们发现,$1-x-x^2=[1-(1-\sqrt{5})x/2]*[1-(1+\sqrt{5})x/2]$,$(1-\sqrt{5})/2$和$(1+\sqrt{5})/2$是$x^2-x-1=0$的两个根。

那么令$x/(1-x-x^2) = \frac{c_1}{1-(1-\sqrt{5})x/2}+\frac{c_2}{1-(1+\sqrt{5})x/2}$

$=\frac{c_1*[1-(1+√5)x/2]+c_2*[1-(1-√5)x/2]}{[1-(1-\sqrt{5})x/2] * [1-(1+\sqrt{5})x/2]}$

$=\frac{c_1*[1-(1+√5)x/2]+c_2*[1-(1-√5)x/2]}{1-x-x^2}$

也就是说,$c_1*[1-(1+√5)x/2]+c_2*[1-(1-√5)x/2]=x$。

代入x=0,得$c_1+c_2=0$;代入x=1,得$\frac{1-\sqrt{5}}{2}c_1+\frac{1+\sqrt{5}}{2}c_2=1$。可以求得$c_1=-1/\sqrt{5},c_2=1/\sqrt{5}$。

所以$x/(1-x-x^2) = \frac{-1/\sqrt{5}}{1-(1-\sqrt{5})x/2}+\frac{1/\sqrt{5}}{1-(1+\sqrt{5})x/2}$

因为$\frac{1}{1-(1-\sqrt{5})x/2}$背后是一个$a_1=1, q=(1-\sqrt{5})x/2$的等比序列;

$\frac{1}{1-(1+\sqrt{5})x/2}$背后是一个$a_1=1, q=(1+\sqrt{5})x/2$的等比序列;

到这里我们就知道$x^n$的系数应该是$f(n)=-(1/\sqrt{5})*[(1-\sqrt{5})/2]^n + (1/\sqrt{5})*[(1+\sqrt{5})/2]^n$。(这就是我们想要的通项公式)

事实上,用上面所说的方法,我们可以求出任何一个线性齐次递推方程的通项公式。什么叫做线性齐次递推呢?就是这样的递推方程:f(n)等于多少个f(n-1)加上多少个f(n-2)加上多少个f(n-3)等等。Fibonacci数列的递推关系就是线性齐次递推关系。


我们最后看一个例子。我们介绍硬币兑换问题:我有1分、2分和5分面值的硬币。请问凑出n分钱有多少种方法。

想一下刚才的水果,我们不难得到这个问题的生成函数:$g(x)=(1+x+x^2+x^3+\ldots)(1+x^2+x^4+\ldots)(1+x^5+x^10+\ldots)=1/[(1-x)(1-x^2)(1-x^5)]$。

现在,把它变成通项公式。我们的步骤同刚才的步骤完全相同。我们把$(1-x)(1-x^2)(1-x^5)$展开,得到$1-x-x^2+x^3-x^5+x^6+x^7-x^8$。

我们求出$-1+x+x^2-x^3+x^5-x^6-x^7+x^8=0$的解,得到了以下8个解:$-1,1,1,1,-(-1)^{1/5},(-1)^{2/5},-(-1)^{3/5},(-1)^{4/5}$。解得$(1-x)(1-x^2)(1-x^5)=(1+x)(1-x)^3(1+(-1)^(1/5) x)()()()$ (省略不写了)。注意那个(1-x)^3。由于等根的出现,我们不得不把(1-x)^3所包含的(1-x)和(1-x)^2因子写进一会儿的分母里,不然会导致解不出合适的c来。你可以看到很多虚数。不过没关系,这些虚数同样参与运算,就像刚才的根式一样不会影响到最后结果的有理性。然后,我们像刚才一样求出常数满足$1/(1-x)(1-x^2)(1-x^5)=c1/()+c2/(1-x)+c3/(1-x)^2+c4/(1-x)^3...+c8/()$。

下列代码给出的是给定arr[] = {硬币面值},凑出target分钱的方法数:

      
         1
      
       #include <iostream>


      
         2
      
       #include <vector>


      
         3
      
      
        using
      
      
        namespace
      
      
         std;


      
      
         4
      
      
         5
      
      
        int
      
       generateFunction(
      
        int
      
       arr[], 
      
        int
      
       n, 
      
        int
      
      
         target) {


      
      
         6
      
      
        if
      
       (n <= 
      
        0
      
      ) 
      
        return
      
       target == 
      
        0
      
       ? 
      
        1
      
       : 
      
        0
      
      
        ;


      
      
         7
      
           vector<vector<
      
        int
      
      > > 
      
        params
      
      (
      
        2
      
      , vector<
      
        int
      
      >(target + 
      
        1
      
      , 
      
        0
      
      
        ));


      
      
         8
      
      
        params
      
      [
      
        0
      
      ][
      
        0
      
      ] = 
      
        1
      
      
        ;


      
      
         9
      
      
        int
      
       cur = 
      
        0
      
      , next = 
      
        1
      
      
        ;


      
      
        10
      
      
        11
      
      
        for
      
       (
      
        int
      
       i = 
      
        0
      
      ; i < n; ++
      
        i) {


      
      
        12
      
      
        params
      
      [next].assign(target + 
      
        1
      
      , 
      
        0
      
      
        );


      
      
        13
      
      
        for
      
       (
      
        int
      
       j = 
      
        0
      
      ; j <= target; ++
      
        j) {


      
      
        14
      
      
        if
      
       (
      
        params
      
      [cur][j] == 
      
        0
      
      ) 
      
        continue
      
      
        ;


      
      
        15
      
      
        for
      
       (
      
        int
      
       k = 
      
        0
      
      ; k + j <= target; k +=
      
         arr[i]) {


      
      
        16
      
      
        params
      
      [next][j + k] += 
      
        params
      
      [cur][j]; 
      
        //
      
      
         * 1
      
      
        17
      
      
                    }


      
      
        18
      
      
                }


      
      
        19
      
               cur = !
      
        cur;


      
      
        20
      
               next = !
      
        next;


      
      
        21
      
      
            }


      
      
        22
      
      
        return
      
      
        params
      
      
        [cur][target];


      
      
        23
      
      
        }


      
      
        24
      
      
        25
      
      
        int
      
      
         main() {


      
      
        26
      
      
        int
      
       n = 
      
        3
      
      
        ;


      
      
        27
      
      
        int
      
       arr[] = {
      
        1
      
      , 
      
        2
      
      , 
      
        5
      
      
        };


      
      
        28
      
      
        29
      
      
        for
      
       (
      
        int
      
       i = 
      
        1
      
      ; i <= 
      
        100
      
      ; ++
      
        i) {


      
      
        30
      
               cout << i << 
      
        "
      
      
        : 
      
      
        "
      
       << generateFunction(arr, n, i) <<
      
         endl;


      
      
        31
      
      
            }


      
      
        32
      
      
        return
      
      
        0
      
      
        ;


      
      
        33
      
       }
    

g(x)=f1(x)*f2(x)*f3(x); f1、f2和f3分别对应于面值为1,2,5的生成函数。我们在化简的时候,首先化简的f1*f2,然后再用(f1*f2)的结果与f3进行化简。Line11-18就是每次化简得到的等式。params[next][i] 表示的是化简得到的式子$x^i$的系数。

我们本来一开始是需要初始化f1对应的系数的。但是注意到一点,如果target=0,且面值为0的情况,f0(x)=1.所以这里我们初始化params[0][0] = 1,其他仍为0.然后通过Line15-17,同样可以初始化f1。

经过n轮之后,整个g(x)就化简出来了。

这里虽然f1、f2、f3都是无限个项,但是为什么Line 13和Line 15只需要循环到target呢?因为对于大于target的项,他们的和也一定会大于target,我们最终只需要$x^{target}$,所以到target为止就行了。

另外,注意Line 15,步进是arr[i],因为只有k+arr[i]的系数为1.


有1克、2克、3克、4克的砝码各一枚,能称出哪几种重量?每种重量各有几种可能方案?

因为只能取一枚,根据前面,我们可以知道生成函数就是$g(x) = (1+x)(1+x^2)(1+x^3)(1+x^4)=1 + x + x^2 + 2x^3 + 2x^4 + 2x^5 + 2x^6 + 2x^7 + x^8 + x^9 + x^{10}$.

所以可以称出0-10的重量,系数就是可能的方案。比如重量为4的可能方案就有2种。


网易游戏2011年笔试 考了这么一道题。尼玛,没搞过ACM的哪懂这个啊。。


 

普通的生成函数对于组合类型数列的研究很有帮助,而指数型母函数可以很方便的拿来研究排列类型的数列。

指数型母函数形式为$g(x) = 1 + f(1)x^1/1!+f(2)x^2/2!+\ldots$

有时可以利用一些级数公式来进行化简推导,

$e^x = 1 + x + x^2 / 2! + x^3 / 3! + … + x^n / n! +\ldots$

这里 google的一道在线笔试题 就是用指数型生成函数的。

母函数问题


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论