生成函数/母函数入门学习

 开始学生成函数了,开篇博客记录一下学习过程,现在还只是学了一些很浅显的内容,还会不断更新。

首先推荐两篇写得很好的博客:https://blog.csdn.net/consciousman/article/details/77935700

https://blog.csdn.net/wu_tongtong/article/details/78854572#comments

 

生成函数或者叫母函数,是解决组合数学尤其是计数问题的一大利器。生成函数有许多种,最为常见也是应用最广泛的两种是普通母函数和指数型母函数。形式上说,普通型生成函数用于解决多重集的组合问题,而指数型母函数用于解决多重集的排列问题。母函数还可以解决递归数列的通项问题(例如使用母函数解决斐波那契数列的通项公式)。(来自百度百科)

普通母函数是用来解决多重集的组合问题的。它的原理是:“母函数的思想很简单—就是把离散数列幂级数一一对应起来,把离散数列间的相互结合关系对应成为幂级数间的运算关系,最后由幂级数形式来确定离散数列的构造. “(来自百度百科)。这句话初听起来很蒙,但是一旦理解了母函数,就能明白这句话就是母函数的本质。

这里给出比较通俗的解释(也许就有失本意了):假如有n个集合,这n个集合元素个数分别是k1,k2,k3...kn,一个集合的元素是相同的。那么要从这n个集合中的元素中取m个组成一个组合(不计顺序)。那么我们构造生成函数A1=(x^0+x^1+x^2+...x^k1) , A2=(x^0+x^1+...x^k2)... An=(x^0+x^1+...x^kn)  然后令 B=(A1)*(A2)*...*(An)。得到的B应该是这样子的,B=a0*x^0+a1*x^1+...an*x^n+...∞

于是B的每一项都是一种组合计算的结果,每一项的幂代表需要组合的元素个数m(即从n个集合中取m个元素),每一项的系数就是选出m个元素的组合方案数。例如第B的第i项为 ai*x^i 代表从n个集合中的i组合数为ai。

生成函数巧妙地运用了多项式乘法的性质,两项相乘:幂数相加,系数相乘。幂代表着选择个数,系数代表着方案数。于是所以n个多项式相乘后的结果就是最终的组合结果。

母函数的时间限制主要是多项式乘法,可以暴力n^2一项一项乘,也可以用nlogn的FFT计算。

暴力算的模板,以HDU1028的整数划分问题为例:

#include<iostream>
#include<cstdio>
using namespace std;
const int N=125;
int n,g[2][N];  //滚动数组保存前i个多项式相乘结果,g[i]代表:指数为i的项的系数为g[i] 

int main()
{
    while (scanf("%d",&n)==1) {
        for (int i=0;i<=n;i++) g[1][i]=1,g[0][i]=0;  //初始化 
        for (int i=2;i<=n;i++) {  //计算前i个砝码的结果 
            for (int j=0;j<=n;j+=i)  //j控制第i个砝码的多项式:(1+x^i+x^2i+...) 
                for (int k=0;j+k<=n;k++)  //k控制前面(i-1)项的结果相乘 
                    g[i%2][j+k]+=g[(i%2)^1][k];    
            for (int j=0;j<=n;j++) g[(i%2)^1][j]=0;            
        }
        printf("%d
",g[n%2][n]);
    }
    return 0;
}

指数型母函数是用来解决多重集的排列问题的。只要理解了普通母函数就很容易理解指数型母函数,指数型母函数只是在初始式子上有些不同。

我们知道多重集的排列公式为:n! / (n1! * n2! * n3! ... nk!) 。普通母函数已经算出了多重集的组合数,在此基础上乘上多重集的排列公式即可解决多重集的排列问题。

那么指数型生成函数为:A1=(x^0/0!+x^1/1!+x^2/2!+...x^k1/k1!) A2=A1=(x^0/0!+x^1/1!+x^2/2!+...x^k2/k2!) .....可以发现指数型母函数只是在每一项除多了个 个数的阶乘。这样计算出来后把结果再乘上其总排列总个数的阶乘,这样便得到了上诉的多重集排列公式。

由于存在出除法,暴力求指数型母函数需要用double类型来存储。

暴力n^2计算指数型母函数,以HDU-1521为例:

#include<bits/stdc++.h>
using namespace std;
const int N=12;
int n,m,a[N];
double f[N],g[2][N];

int main()
{
    while (cin>>n>>m) {
        memset(g,0,sizeof(g));
        for (int i=1;i<=n;i++) scanf("%d",&a[i]);
        f[0]=1; for (int i=1;i<=m;i++) f[i]=f[i-1]*i;
        
        for (int j=0;j<=a[1];j++) g[1][j]=1.0/f[j];
        for (int i=2;i<=n;i++) {  //前i个集合 
            for (int j=0;j<=a[i];j++)  //j为第i个集合的幂 
                for (int k=0;j+k<=m;k++)  //k为前i-1个集合的幂 
                    g[i%2][j+k]+=g[(i%2)^1][k]/f[j];
            for (int j=0;j<=m;j++) g[(i%2)^1][j]=0;        
        }
        printf("%.0lf
",f[m]*g[n%2][m]);
    }
    return 0;
}

母函数的化简:以上其实只是最最最基础的内容。学会了以上内容其实也不能做很多的题,因为常常会有的题目的N十分巨大,即使你看出了朴素的母函数,但是也不能在规定时间内求出来(即使用FFT),或者这个母函数根本就没办法算,这时候就需要化简母函数使他变得更简单。

化简母函数需要积累大量的常见化简公式,这一块还是看上面两个大佬的博客吧,大佬写得很详细。

题目练习

原文地址:https://www.cnblogs.com/clno1/p/10813884.html