母函数入门
一、定义介绍:
母函数,又称生成函数,是ACM竞赛中经常使用的一种解题算法,常用来解决组合方面的题目。
在数学中,某个序列的母函数(Generating function,又称生成函数)是一种形式幂级数,其每一项的系数可以提供
关于这个序列的信息。使用母函数解决问题的方法称为母函数方法。
母函数可分为很多种,包括普通母函数、指数母函数、L级数、贝尔级数和狄利克雷级数。
对每个序列都可以写出以上每个类型的一个母函数。构造母函数的目的一般是为了解决某个特定的问题,因此选用何种母函数视乎序列本身的特性和问题的类型。
这里先给出两句话
1.“把组合问题的加法法则和幂级数的乘幂对应起来”
2.“母函数的思想很简单 ——就是把离散数列和幂级数一一对应起来,把离散数列间的相互结合关系对应成为幂级数间的
运算关系,最后由幂级数形式来确定离散数列的构造. ”
母函数用一维数组就可以表示: a[ ] 下标表示组成的数量,值表示方法数
我们首先来看下这个多项式乘法:
由此可以看出:
1.x的系数是a1,a2,…an 的单个组合的全体。
2. x^2的系数是a1,a2,…a2的两个组合的全体。
………
n. x^n的系数是a1,a2,….an的n个组合的全体(只有1个)。
进一步得到:
母函数的定义
对于序列a0,a1,a2,…构造一函数:
称函数G(x)是序列a0,a1,a2,…的母函数。
二、问题引入
1、第一种例题:
- 假设我们当前有质量分别为 1g, 2g, 3g, 4g 的砝码 各一个 , 我们可以构造出多少种质量为7g的砝码方案?
基础解法
- 显然, 想要指定重量的砝码序列的构造, 对于每一个砝码, 我们都有使用或者不使用两种选择. 如果我们对每个砝码进行枚举的话, 则共需要枚举
种选择, 即这种解法的时间复杂度为
.
代数解法
-
考虑用母函数来解决这个问题:
我们假设x表示砝码,x的指数表示砝码的重量,这样:
1个1克的砝码可以用函数1+1*x^1表示,
1个2克的砝码可以用函数1+1*x^2表示,
1个3克的砝码可以用函数1+1*x^3表示,
1个4克的砝码可以用函数1+1*x^4表示,
上面这四个式子懂吗?
我们拿1+x^2来说,前面已经说过,x表示砝码,x的指数表示砝码的重量!初始状态时,这里就是一个质量为2的砝码。
那么前面的1表示什么?按照上面的理解,1其实应该写为:1*x^0,即1代表重量为2的砝码数量为0个。
所以这里1+1*x^2 = 1*x^0 + 1*x^2,即表示2克的砝码有两种状态,不取或取,不取则为1*x^0,取则为1*x^2
不知道大家理解没,我们这里结合前面那句话:
“把组合问题的加法法则和幂级数的乘幂对应起来“
接着讨论上面的1+x^2,这里x前面的系数有什么意义?
这里的系数表示状态数(方案数) ( 幂级数前边的系数表示的是 方案数)
1+x^2,也就是1*x^0 + 1*x^2,
-
也就是上面说的不取2克砝码,此时有1种状态;或者取2克砝码,此时也有1种状态。(分析!)
所以,前面说的那句话的意义大家可以理解了吧?
几种砝码的组合可以称重的情况,可以用以上几个函数的乘积表示:
G(x)= // 母函数表示的是 每克只有一个,取或不取
-
(1+x)(1+x^2)(1+x^3)(1+x^4)
-
=(1+x+x^2+x^4)(1+x^3+^4+x^7)
-
=1 + x + x^2 + 2*x^3 + 2*x^4 + 2*x^5 + 2*x^6 + 2*x^7 + x^8 + x^9 + x^10
从上面的函数知道:可称出从1克到10克,系数便是方案数。(!!!经典!!!)
例如右端有2^x^5 项,即称出5克的方案有2种:5=3+2=4+1;同样,6=1+2+3=4+2;10=1+2+3+4。
故称出6克的方案数有2种,称出10克的方案数有1种 。
2、第二种例题:
求用1分、2分、3分的邮票贴出不同数值的方案数:
大家把这种情况和第一种比较有何区别?第一种每种是一个,而这里每种是无限的。
母函数表示的是每一个分额的邮票可以表示的数值
例如第一个括号: 1 分的可以表示 1分,2分,3分,4分,5分,6分......
第二个括号: 2 分的可以表示 2分,4分,6分,8分,10分......
第三个括号: 3 分的可以表示 3分,6分,9分,12分,15分......
以展开后的x^4为例,其系数为4,即4拆分成1、2、3之和的拆分方案数为4;
即 :4=1+1+1+1=1+1+2=1+3=2+2
母函数通常解决类似如下的问题:
给5张1元,4张2元,3张5元,要得到15元,有多少种组合?
某些时候会规定至少使用3张1元、1张2元、0张5元。
某些时候会规定有无数张1元、2元、5元
……
三、解题过程
解题时,首先要写出表达式,通常是多项的乘积,每项由多个x^y组成。
如(1+x+x^2)(1+x^4+x^8)(x^5+x^10+x^15)
通用表达式为
(x^(v[0]*n1[0])+x^(v[0]*(n1[0]+1))+x^(v[0]*(n1[0]+2))+...+x^(v[0]*n2[0]))
(x^(v[1]*n1[1])+x^(v[1]*(n1[1]+1))+x^(v[1]*(n1[1]+2))+...+x^(v[1]*n2[1]))
...
(x^(v[K]*n1[K])+x^(v[K]*(n1[K]+1))+x^(v[K]*(n1[K]+2))+...+x^(v[K]*n2[K]))
K 对应具体问题中物品的种类数。
v[i] 表示该乘积表达式第 i 个因子的权重,对应于具体问题的每个物品的价值或者权重。
n1[i] 表示该乘积表达式第i个因子的起始系数,对应于具体问题中的每个物品的最少个数,即最少要取多少个。
n2[i] 表示该乘积表达式第i个因子的终止系数,对应于具体问题中的每个物品的最多个数,即最多要取多少个。
对于表达式(1+x+x^2)(x^8+x^10)(x^5+x^10+x^15+x^20)
v[3]={1,2,5},n1[3]={0,4,1},n2[3]={2,5,4}。
解题的关键是要确定v、n1、n2数组的值。
通常n1都为0,但有时候不是这样。
n2有时候是无限大。
四、CODE :
// 本题是 HDU 1398 的代码,所以循环中的 17 只针对本题,具体题目具体分析
一般可以改成
for (int i = 2; i <= n; i++)
{
for (int j = 0; j <= n; j++)
{
for (int k = 0; k + j <= n; k += i)
b[k + j] += a[j];
}
}
#include<cstdio>
#include<iostream>
using namespace std;
const int MAXN = 300 + 10;
int a[MAXN];
int b[MAXN];
int main()
{
int n;
while(cin >> n && n)
{
for(int i = 0;i <= 300;i++) // 初始化 ————————————1
{
a[i] = 1; // 模拟第一个括号,各项的系数为1
b[i] = 0; // 中间数组
}
for(int i = 2;i <= 17;++i) // 外层括号数,17项母函数——————2
{
for(int j = 0;j <= n;++j) // 合并前两个式子,这里前两个式子代表每次交换后的两个————————3
{
for(int k = 0;k + j <= n;k += i * i)// 增量——————————4
{
b[k + j] += a[j]; // 将相同次幂的系数合到一起
}
}
for(int j = 0;j <= n;++j) // 清空C 继续接受下一个式子————————————5
{
a[j] = b[j];
b[j] = 0;
}
}
cout << a[n] << endl;
}
return 0;
}
解释代码:
① 、首先对c1初始化,由第一个表达式(1+x+x^2+..x^n)初始化,把质量从0到n的所有砝码都初始化为1.
② 、 i从2到n遍历,这里 i 就是指第i个表达式,上面给出的第二种母函数关系式里,每一个括号括起来的就是一个表达式。
③、j 从0到n遍历,这里j就是(前面几个表达式累乘的表达式)里第j个变量,如 (1+x)(1+x^2)(1+x^3),j 先指示的是1和x的系数, i=2执行完之后变为 (1+x+x^2+x^3)(1+x^3),这时候 j 应该指示的是合并后的第一个括号的四个变量的系数。
④ 、 k 表示的是第 j 个指数,所以 k 每次增 i(因为第i个表达式的增量是i)
例如第二种方法中的第二个括号 (1+x^4+x^8) 指数是以 2增加的 ( 针对本题)
⑤ 、把c2的值赋给c1,而把c2初始化为0,因为c2每次是从一个表达式中开始的。
17个括号,先前两个结合成为一个,再与第三个结合,一次进行下去