在用到母函数之前啊,恐怕很少有人听过母函数,我也一样,我当时是做杭电acm的2082题要用母函数做才去了解母函数的。当然母函数分为很多种,包括普通母函数、指数母函数、L级数、贝尔级数和狄利克雷级数,我这里说的都是普通母函数。
什么是普通母函数呢,——把组合问题的加法法则和幂级数的的乘幂的相加对应起来,这句话可能一开始难以理解,不过其实学完了之后很容易理解,母函数的思想很简单—就是把离散数列和幂级数一一对应起来,把离散数列间的相互结合关系对应成为幂级数间的运算关系,最后由幂级数形式来确定离散数列的构造。
我就从那个经典的砝码的例子讲起。
题目:有1克、2克、3克、4克的砝码各一 枚,能称出哪几种重量?每种重量各有几种可能方案?
穷举的话很容易就可以得出结果,但是很显然这种类型的穷举需要花费的时间是n4方级别,计算机还可以在比较短的而时间内算出来,但是如果给出m种砝码,那么就是nm次方级别,那么如果砝码种类一多,计算机恐怕就无法在短时间内给出答案。而通过用母函数可以把这类题的时间规模压缩到n3级别,使得计算机能很快给出解答。
我们先来看看穷举的过程,先假设使用一个1g的砝码,然后再假设在此情况下,使用2g的砝码,在此基础上,再假设使用一个3g的,再假设使用一个4g的,这就是1中情况,显然,这种方案称出的重量为10g,然后改变上面的一个假设条件,比如假设没有使用4g的,只是用了1g、2g、3g,显然这种方案称出的是6g,以此类推,将所有的可能列举出来后即可得到全部的方案,1g的砝码有两种情况,使用还是不使用,其他的砝码也一样,每一种1g砝码的状态都可以有两种砝码状态,以此类推总共就有2*2*2*2=16中方案,当然其中包含一种一个砝码也没有的0g方案。
我们最后可以穷举出结果:1g、2g、8g、9g、10g各有一种方案,3g、4g、5g、6g、7g各有两种,总共十五种,这是在不算一种0g的方案的时候,算上就是16种,和上面的过程相符。
我们可以用一个类似离散数学的逻辑式表示前两种砝码组合出来的情况
我在这里使用||表示析取,也就是编程时的“逻辑或”;用&&表示合取,也就是编程时的“逻辑与”,析取与合取都是离散数学中的概念,其实就等于编程语言中的逻辑与和逻辑或,析取与合取的符号ˆ和ˇ很多人虽学过并不熟悉,我就用编程语言中的符号代替了,熟悉离散数学的人可以转换成析取与合取的符号。
(使用1g || 不使用1g)&&(使用2g || 不使用2g)
=使用1g&&使用2g || 不使用1g&&使用2g || 使用1g&&不使用2g || 不使用1g&&不使用2g
每个用 “||”分开的一项都是一种方案。“&&”表示选择第一项的情况下有选择了第二项。
大家可以发现这个表达式和一种表达式很像,没错,如果把“||”看成加法,“&&”看成乘法,和多项式的乘法一模一样。那么我们直觉的想到,有没有可能用多项式乘法来表示组合的情况呢?我们再来看题目,题目需要的是几种砝码组合后的重量,是一个加法关系,但是在上式中“&&”是一种类似于乘法的运算关系,这怎么办呢?有没有什么这样一种运算关系,以乘法的形式运算,但是结果表现出类似于加法的关系呢?正好有一个,那就是幂运算。Xm 乘上Xn结果是Xm+n,他完美的符合了我们的要求。那么以次数表示砝码的质量, 就可以以多项式的形式表示砝码组合的所有方案。
还是以前两个砝码为例说明,表示1g砝码的两种状态的多项式就是(x0+x1),表示2g的就是(x0+x2),x0表示没有使用砝码,所以重量为0,当然了因为x0 =1,所以也可以写成1,后面为了方便我会这么写。注意,砝码的质量是以次数表示的,而不是直觉上的用下标表示为x1,x2。这点很重要,不然的话就无法表现出幂运算的关系了。
(x0+x1)*(x0+x2)
=x0*x0 + x1*x0 + x0*x2 + x1*x2
=x0 + x1 + x2 + x3
很显然,有四种方案,0g、1g、2g、3g,结果与我们穷举的结果相同,而如果结果中有相同的项,那么合并同类项后每一项的系数就是这种重量有几种实现方案,这会在我们用此方法解决4个砝码问题的时候得到证明。
那么接下来试试用这种表达式表示4个砝码的组合情况
(x0+x1)* (x0+x2) * (x0+x3)* (x0+x4)
=x0 + x1 + x2 + 2x3 + 2x4 + 2x5 + 2x6 + 2x7 + x8+ x9 + x10
具体的计算过程我就不细写了,多项式乘法大家都应该会。
结果与上面穷举的结果是相同的,次数代表组合后可称出的质量,系数代表组合出这种质量的方案的数量。至此也就得出了答案。这就是普通母函数,现在你可以回头去看看我前面说的那两句话——把组合问题的加法法则和幂级数的的乘幂的相加对应起来,把离散数列间的相互结合关系对应成为幂级数间的运算关系,最后由幂级数形式来确定离散数列的构造。是不是明白了母函数的本质了呢?是不是恍然大悟了呢?
接下来我们把开始的题目稍稍变化一下:
求用1g、2g、3g的砝码称出不同重量的方案数 。
大家把这种情况和第一种比较有何区别?第一种每种是一个,而这里每种是无限的。
怎样在母函数里表现出无限这种性质呢?很简单,我们以2g砝码为例,因为我们有无限个2g砝码,所以我们可以把2个2g砝码看成是4g砝码,3个2g砝码看成是6g砝码,依次类推,把m个n g砝码看成是一个n*m g砝码,还是先以前两个砝码为例,那么多项式相应的就变成
(x0 +x1 + x2 + x3 + x4 + x5 + …… )*(x0 + x2 + x4 + x6 + x8+ x10 + ……)
结果自然也是无限的,但是这种问题在实际问题中,一定会给出一个确定的值,比如说求组成10g的方案有几种。那么我们就只要在合并后的结果求到最高次的项是x10即可,后面的项可以忽略不计。那么要结果中最高为10次方,开始每一种砝码的无限项的表达式该写到几次呢?也是10次,因为表达式中最低的项有x0 ,所以想在结果中不漏掉出现x10 的项,必须乘之前的项最高的项不能小于x10,而表达式中不可能出现x-1,,所以x11 和任何一项相乘都会大于x10,所以x11 是不需要的,但写上也无妨,不影响结果,但是如果可以有x10(比如3g的砝码组合不出10g的,但是2g的就可以),那就必须写,不然就会漏掉一些方案。
那么如果题目是求用1g、2g、3g的砝码称出10g的方案数 。
表达式就是:
(x0 +x1 + x2 + x3 + x4 + x5 + ……x10 )*(x0 + x2 + x4 + x6 + x8+ x10 )
结果就是合并同类项后x10的系数。
多项式乘法显然是一种运算时间规模在n2级别的运算,但是如果循环生成无限的砝码,也就是上面用小砝码组合出每一种可能的大砝码,那么生成多项式就又需要n的规模,在此基础上进行多项式乘法,最后无限砝码的问题需要的运算时间规模就是n3级别。
这样母函数就把这类组合问题从nn级别转化成了n3级别。这便是母函数的奇妙与威力。
知道了母函数的原理,用程序来实现也就不是什么困难的事了,其实就是做多项式乘法的程序
用c++实现算出无限砝码情况下的某种重量的方案数。
#include <iostream>
using namespace std;
// Author: bjr
//
const int max = 1000;
// sup是保存多项式的数组,sup[n]中的值代表xn的系数
// temp是临时多项式,保存相乘的临时中间情况
int sup[max], temp[max];
/*
程序始终只计算两个多项式之间的乘积,多个多项式的情况
先计算前两个的乘积,将结果作为第一个多项式,再与第三个相乘
依次类推,sup始终存放当前运算后的结果然后作为被乘多项式,
*/
int main()
{
int target; // 目标重量, 比如上面的例子里就是10,要<max的值
int i, j, k;
while(cin >> target)
{
for(i=0; i<=target; ++i)
{
sup[i] = 1;
//初始化第一个多项式,也就是用1g砝码的多项式,
//注意如果题目没给1g的砝码那么就不能++i,而要加上砝码的质量
temp[i] = 0;
//将临时区清空,无论第一个多项式质量是几都要全部置零
}
for(i=2; i<=target; ++i)
// 生成后续的第i个多项式,此题中是2g,i从2开始。
//如果砝码的值不是规律增长,i可能需要取决于输入
{
for(j=0; j<=target; ++j)
// 遍历当前结果多项式的每一项(当前结果的第j项)与第i个多项式相乘,
for(k=0; k+j<=target; k+=i)
// 遍历第i个多项式的每一项,此处构造用小砝码组成大砝码的多项式
{
temp[j+k] += sup[j];
//幂运算,注意理解
}
for(j=0; j<=target; ++j)
// 将临时的结果覆盖当前结果,同时把临时结果置零,为下次做准备
{
sup[j] = temp[j];
temp[j] = 0;
}
}
cout << sup[target] << endl; //输出结果
}
return 0;
}