生成函数

思考问题: 如果有10种不同颜色的球各一个,从中取两个,不同的取球方案是多少? 简单的排列组合问题,很轻松就能C102C_{10}^2 = 45 但如果换一种问题,有面值1、2、3、4、5、...、99、100元的硬币各一枚,那么有多少种组成方案?

# 😄普通型母函数

# 📕前置芝士

# 🎈幂相乘

幂次相乘 = 指数相加
xaxb=xa+bx^{a} * x^{b} = x^{a + b}

# 🎈Hash

将某个数存入某个地址内从而完成某种映射关系,并可以O(1)O(1)的时间内查找到和进行操作
母函数算法某种程度上利用了hash的,了解hash可以更深刻地理解该算法
Hash传送门 (opens new window)

# 📕概述

# 🎈概念 & 用处

母函数分为普通型母函数与指数型母函数
普通型母函数用于解决多重集的组合问题
指数型母函数用于解决多重集的排列问题

在某种意义上可以联系起来背包问题进行思考
两者有互通之处但同时又能解决两种不同的问题

我们定义一个序列a0a1a2...a_0、a_1、a_2、...并构造一函数
Gx=a0+a1x+a2x2+...G_x = a_0 + a_1x + a_2x^2 + ...
则称GxG_x是序列a0a1a2...a_0、a_1、a_2、...的母函数

同时如果已知函数Gx=(1+x)2G_x = (1 + x)^2
则在展开后可以得到Gx=1+2x+x2G_x = 1 + 2x + x^2
则有该函数是序列1211、2、1的母函数

# 🎈构造表示

例如我们规定:有1元硬币,3元硬币,5元硬币,问都可以构造出来什么数值
我们利用以下规则:
每一种硬币都为一个小括号内的多项式
同时将硬币面值x作为指数(从0开始,x^1^,x^2^,...)

在这里我们构建1元硬币的多项式:1+x+x2+x3+x4+x5+...1 + x + x^2 + x^3 + x^4 + x^5 + ...
我们能很轻松发现1元硬币能构造出的数值为0、1、2、3、4、5、...
构建3元硬币的多项式:1+x3+x6+x9+...1 + x^3 + x^6 + x^9 + ...
我们可以发现3元硬币构造出的数值为0、3、6、...
构建五元硬币的多项式:1+x5+x10+....1 + x^5 + x^{10} + ....
得到数值0、5、10、...

在得到所有硬币能构造出来的数值时,我们利用这几个多项式相乘
(1+x+x2+x3+x4+x5+...)(1+x3+x6+x9+...)(1+x5+x10+....)=1+x1+x2+2x3+2x4+3x5+4x6+4x7+5x8+6x9+7x10+.....(1 + x + x^2 + x^3 + x^4 + x^5 + ...) * (1 + x^3 + x^6 + x^9 + ...) * (1 + x^5 + x^{10} + ....) = 1 + x^1 + x^2 + 2x^3 + 2x^4 + 3x^5 + 4x^6 + 4x^7 + 5x^8 + 6x^9 + 7x^{10} + .....
bxabx^a表示有b种不同的方法能构造出来数值a
其中每一种情况都能有对应的结果,但同时不同的情况构造出来同样的数值时,这两种情况又会累加,所以我们可以得到都能构建出什么数值,与构建出来某种数值有几种方法

# 🎈过程

建立两个数组c1,c2,c1~i~表示前面括号合并后指数i的系数,c2~i~表示下一个准备合并的括号的指数i的系数
1.初始化第一个多项式
2.多项式两两合并(括号的合并){
    数值映射累加给c2,c2[第一个括号某个指数 + 第二个括号某个指数] += c1[第一个括号某个指数]
    c2的值赋给c1,c2清空
}

# 📕程序实现

const int maxn=10000;
int c1[maxn + 1];//c1[i]表示母函数第一个小括号内的表达式中,指数为i的系数
int c2[maxn + 1];//第二个的系数(职业备胎(狗头))
int main(){
    int n;//组合出来n
    while(cin >> n)
    {
        memset(c1, 0, sizeof(c1));
        memset(c2, 0, sizeof(c2));
        for(int i = 0; i <= n; i += elem[1]) c1[i] = 1;//对第一个括号的内容进行赋1
        
        //=========================================主体
        for(int i = 2; i <= n; i ++)//做n-1趟前两括号合并
        {
            for(int j = 0; j <= n; j ++)//第1个括号
            {
                for(int k = 0; k + j <= n; k += elem[i])//第2个括号,(k+=i)是因为下一个括号指数会+=i
                     c2[j + k] += c1[j];//合并后系数相加,要继承一下上一个
            }
            for(int j = 0; j <= n; j ++)//替换一下第一个括号数组,初始化下一个括号数组
            {
                c1[j] = c2[j];
                c2[j] = 0;
            }
        }
        //=============================================
        cout << c1[n] << endl;//此时就剩一个括号,c1[n]即为合并结束后的n次方系数
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 😄指数型母函数

思考:
考虑n个元素组成的多重集,其中a[1]重复了n[1]次,a[2]重复了n[2]次,...,a[k]重复了n[k]次,n = n[1] + n[2] + ... + n[k]。
现在从中取r个进行排列,求不同的排列数
解:若r = n,即考虑n个元素的全排列,则不同的排列数显然是 n!/n[1]!/n[2]!/.../n[k]!

深入实例:
假设有8个元素,其中a[1]重复3次,a[2]重复2次,a[3]重复3次 从中取r个组合,其组合数为c[r],则其对应母函数为:
Gx=(1+x+x2+x3)(1+x+x2)(1+x+x2+x3)G_x = (1 + x + x^2 + x^3) * (1 + x + x^2) * (1 + x + x^2 + x^3)
=(1+2x+3x2+3x3+2x4+x5)(1+x+x2+x3)\quad\;\;= (1 + 2x + 3x^2 + 3x^3 + 2x^4 + x^5)*(1 + x + x^2 + x^3)
=1+3x+6x2+9x3+10x4+9x5+6x6+3x7+x8\quad\;\;= 1 + 3x + 6x^2 + 9x^3 + 10x^4 + 9x^5 + 6x^6 + 3x^7 + x^8

分析x4x^4项组成,会发现表示了从8个元素取4个的组合方案,例如x1x33,x12x32....x_1 * x_3^3,x_1^2 * x_3^2....
讨论排列数:x12x32x_1^2 * x_3^2的不同排列数为4!2!2!\frac{4!}{2! * 2!}x1x33x_{1} * x_{3}^3的不同排列数为4!1!3!\frac{4!}{1!*3!}
所以所有的不同排列数为:
4!(11!3!+11!3!+12!2!+11!1!2!+12!2!+13!1!+12!1!1!+11!2!1!+13!1!+12!2!=16+18+36=704! * (\frac1{1! * 3!} + \frac1{1! * 3!} + \frac1{2!*2!} + \frac1{1!*1!*2!} + \frac1{2!*2!} + \frac1{3!*1!} + \frac1{2!*1!*1!} + \frac1{1!*2!*1!} + \frac1{3!*1!} + \frac1{2!*2!} = 16+18+36 = 70

# 📕概述

# 🎈定义 & 用处

Gx=(1+x1!+x22!+x22!)(1+x1!+x22!)(1+x1!+x22!+x33!)G_x = (1 + \frac x{1!} + \frac {x^2}{2!} + \frac {x^2}{2!})(1 + \frac x{1!} + \frac{x^2}{2!})(1 + \frac x{1!} + \frac {x^2}{2!} + \frac {x^3}{3!})
=1+3x+92x2+143x3+3512x4+1712x5+3572x6+872x7+172x8\quad\;\;= 1 + 3x + \frac 92x^2 + \frac{14}3x^3 + \frac{35}{12}x^4 + \frac{17}{12}x^5 + \frac{35}{72}x^6 + \frac8{72}x^7 + \frac1{72}x^8
=1!+31!x+92!x2+283!x3+704!x4+1705!x5+3506!x6+5607!x7+5608!x8\quad\;\;= 1! + \frac3{1!}x + \frac9{2!}x^2 + \frac{28}{3!}x^3 + \frac{70}{4!}x^4 + \frac{170}{5!}x^5 + \frac{350}{6!}x^6 + \frac{560}{7!}x^7 + \frac{560}{8!}x^8
标准化为ab!xb\frac a{b!}x^b的a便是方案数

对于序列a0,a1,a2,...,a_0,a_1,a_2,...,
函数Gx=a0+(a11!x+a22!x2+a33!x3+...+akk!xk+..)G_x = a_0 + (\frac {a_1}{1!}x + \frac{a_2}{2!}x^2 + \frac {a_3}{3!}x^3 + ... + \frac {a_k}{k!}x^k + .. )
称为序列a0,a1,a2a_0,a_1,a_2...对应的指数型母函数
这样对于一个多重集,其中a1a_1重复n1n_1次,a2a_2重复n2n_2次,...,aka_k重复nkn_k次,从中取r个排列的不同排列数对应的指数型母函数为:
Gx=(1+x1!+x22!+...+xn1n1!)(1+x1!+x22!+...+xn2n2!)...(1+x1!+x22!+...+xnknk!)G_x = (1 + \frac {x}{1!} + \frac {x^2}{2!} + ...+ \frac {x^{n_1}}{n_1!}) * (1 + \frac {x}{1!} + \frac {x^2}{2!} + ...+ \frac {x^{n_2}}{n_2!}) * ... * (1 + \frac {x}{1!} + \frac {x^2}{2!} + ...+ \frac {x^{n_k}}{n_k!})

# 🎈过程 & 构造表示

与普通型母函数差不多,只需要注意分母的阶乘化简即可

# 📕程序实现

const int N = 21;
double c1[N], c2[N];//注意类型
int val[N], F[N];
void Factorial(){
    F[0] = 1;
    for(int i = 1; i <= 20; i ++) F[i] = F[i - 1] * i;
}
int main(){
    Factorial();
    while(scanf("%d%d", &n, &m) == 2){
        for(int i = 0; i < n; i ++) scanf("%d", &val[i]);
        memset(c1, 0, sizeof(c1));
        memset(c2, 0, sizeof(c2));
        for(int i = 0; i <= val[0]; i ++) c1[i] = 1.0 / F[i];
        for(int i = 1; i < n; i ++){
            for(int j = 0; j <= n; j ++){
                for(int k = 0; k + j <= m && k <= val[i]; k ++) c2[j + k] += c1[j] / F[k];//多了一个对阶乘的除法
            }
            for(int j = 0; j <= m; j ++) c1[j] = c2[j], c2[j] = 0;
        }
        printf("%.0f\n",c1[m] * F[m]);//在这里对除的阶乘进行恢复
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Last Updated: 10/14/2023, 7:51:49 PM