算法学习笔记:母函数详解
引言
母函数(Generating function,生成函数)是组合数学中一种重要的方法,这里只对最简单的普通母函数作简单介绍。其主要思想是,把离散序列和幂级数对应起来。
先来看一个最经典的例子:给你1克、2克、3克、4克的砝码各一枚,问称出1~10克的方案分别有多少种?
用母函数的方法,只需要算一个式子就好了:
n次项表示称出n克的方案,例如当n=7时有两种方案(3+4与1+2+4)。
我们用幂级数 表示一种状态,它的每一项表示有 种方法称出 克。例如 ,其实应该写成 ,它的含义是,若你拥有1个2克砝码,有1种方法称出0克,0种方法称出1克,1种方法称出2克,……
当我们把两个幂级数相乘时,我们相当于把两种状态的砝码拿到了一起:
这完全就是乘法原理,如果有 种方法称出 克, 种方法称出 克,当然就有 种方法称出 克。但是 可能不只有一种表示方法,我们把各种情况都加了起来,这又是加法原理。
于是,我们把看上去有点困难的组合学问题转化为了代数问题。
我们来看例题:
(HDU1028 Ignatius and the Princess III)
"Well, it seems the first problem is too easy. I will let you know how foolish you are later." feng5166 says.
"The second problem is, given an positive integer N, we define an equation like this:
N=a[1]+a[2]+a[3]+...+a[m];
a[i]>0,1<=m<=N;
My question is how many different equations you can find for a given N.
For example, assume N is 4, we can find:
4 = 4;
4 = 3 + 1;
4 = 2 + 2;
4 = 2 + 1 + 1;
4 = 1 + 1 + 1 + 1;
so the result is 5 when N is 4. Note that "4 = 3 + 1" and "4 = 1 + 3" is the same in this problem. Now, you do it!"
Input
The input contains several test cases. Each test case contains a positive integer N(1<=N<=120) which is mentioned above. The input is terminated by the end of file.
Output
For each test case, you have to output a line contains an integer P which indicate the different equations you have found.
就是问一个正整数可以被用多少种方式划分为其他正整数的和。我们可以把它转化为:我们有无限个任意正整数重量的砝码,有多少方式称出n克?
我们如果有无穷多个1克砝码,对应的母函数是什么样子?答案是 ,也就是称出任何自然数重量都有1种方法。相应的,如果有无穷多个2克砝码,对应的母函数是 。
所以我们要算的就是 ,这下不好办了,无穷项的幂级数求积可不好处理。但是呢,注意到数据范围是N<=120,那就好办了,我们只需要考虑120次项以内的情况。
大部分题解讲到这里给出的程序,乍一看跟上面讲的母函数关系不大,倒有点像DP,其实那算是优化过的,我想给出一份思路直接一些的代码:
#include <cstring>
#include <iostream>
using namespace std;
int pol[123][123], ans[123] = {1}, cur[123];
int main()
{
for (int i = 1; i <= 120; ++i)
for (int j = 0; j <= 120; j += i)
pol[i][j] = 1; // 初始化各个多项式
for (int i = 1; i <= 120; ++i) // ans依次乘上所有多项式
{
for (int j = 0; j <= 120; ++j)
for (int k = 0; j + k <= 120; ++k) // 注意防止越界
cur[j + k] += ans[j] * pol[i][k]; // 类似于朴素高精度乘法,还不用进位
memcpy(ans, cur, sizeof(cur));
memset(cur, 0, sizeof(cur));
}
int q;
while (cin >> q)
cout << ans[q] << endl;
return 0;
}
如果不用cur,ans需要在计算过程中自己乘自己,很可能发生错误,所以我们用cur作为临时数组保存中间结果。每趟多项式乘法结束后,把cur复制给ans然后清空cur。内层循环就是朴素的多项式乘法,可以用FFT、NNT优化,但这里没必要。
再看另一道题目:
(HDU1398 Square Coins)
People in Silverland use square coins. Not only they have square shapes but also their values are square numbers. Coins with values of all square numbers up to 289 (=17^2), i.e., 1-credit coins, 4-credit coins, 9-credit coins, ..., and 289-credit coins, are available in Silverland.
There are four combinations of coins to pay ten credits:ten 1-credit coins,
one 4-credit coin and six 1-credit coins,
two 4-credit coins and two 1-credit coins, and
one 9-credit coin and one 1-credit coin.Your mission is to count the number of ways to pay a given amount using coins of Silverland.
Input
The input consists of lines each containing an integer meaning an amount to be paid, followed by a line containing a zero. You may assume that all the amounts are positive and less than 300.
Output
For each of the given amount, one line containing a single integer representing the number of combinations of coins should be output. No other characters should appear in the output.
就是某个国家的硬币面额有1、4、9……元,问某个价格可以用多少种方式给出。这和刚刚那道题基本没有什么区别,直接计算 :
#include <cstring>
#include <iostream>
using namespace std;
int pol[18][305], ans[305] = {1}, cur[305];
int main()
{
for (int i = 1; i <= 17; ++i)
for (int j = 0; j <= 300; j += i * i)
pol[i][j] = 1;
for (int i = 1; i <= 17; ++i)
{
for (int j = 0; j <= 300; ++j)
for (int k = 0; j + k <= 300; ++k)
cur[j + k] += ans[j] * pol[i][k];
memcpy(ans, cur, sizeof(cur));
memset(cur, 0, sizeof(cur));
}
int q;
while (cin >> q && q != 0)
cout << ans[q] << endl;
return 0;
}
砝码有限的情形:
(洛谷P2347 砝码称重)
设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重 ),
输入格式
输入方式:
(表示1g砝码有 个,2g砝码有 个,…,20g砝码有个)
输出格式
输出方式:Total=N
(N表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)
这道题其实完全没必要用母函数(上面那几道题也可以不用母函数,DP可解),但母函数也可做:
#include <cstring>
#include <iostream>
using namespace std;
int n, cnt, pol[6][1005], ans[1005] = {1}, cur[1005], step[] = {1, 2, 3, 5, 10, 20};
int main()
{
for (int i = 0; i < 6; ++i)
{
cin >> n;
for (int j = 0; n-- > -1; j += step[i]) // 循环n+1次
pol[i][j] = 1;
}
for (int i = 0; i < 6; ++i)
{
for (int j = 0; j <= 1000; ++j)
for (int k = 0; j + k <= 1000; ++k)
cur[j + k] += ans[j] * pol[i][k];
memcpy(ans, cur, sizeof(cur));
memset(cur, 0, sizeof(cur));
}
for (int i = 1; i <= 1000; ++i)
if (ans[i])
cnt++;
cout << "Total=" << cnt;
return 0;
}
(这显然可以优化,因为乘了大量的0,不过数据范围小无所谓了)
我们也来看看其他资料给出的母函数模板是什么意思,仍以数字划分为例:
#include <cstring>
#include <iostream>
using namespace std;
int ans[123], cur[123];
int main()
{
for (int i = 0; i <= 120; ++i)
ans[i] = 1; // 第一个多项式:各项系数全部为1
for (int i = 2; i <= 120; ++i) // 从第2个多项式开始计算
{
for (int j = 0; j <= 120; ++j)
for (int k = 0; j + k <= 120; k += i) // 以当前的i为步进
cur[j + k] += ans[j];
memcpy(ans, cur, sizeof(cur));
memset(cur, 0, sizeof(cur));
}
int q;
while (cin >> q)
cout << ans[q] << endl;
return 0;
}
例如当计算 时,就是把它看作:
这相当于把该多项式的各次项系数分别向右移0、2、4……位,然后再按次数相加。
这种方法时间上与我的方法相差不大,但它没有把每个多项式储存下来,节约了空间。可以认为是利用数据特点进行的特殊优化,有一定的局限性。
母函数还有一个常用的特性:可以用等比数列求和公式把它们转化成分式,分式相乘后再转换回多项式。无限项的情形,可以直接假设 然后求一个极限(例如 )。因为这个算法并不真的用得上 的值,所以可以这样假设。这样算,有时能把问题简化不少。例如下面这道题:
为了拯救世界,小 a 和 uim 决定召唤出 kkksc03 大神和 lzn 大神。根据古籍记载,召唤出任何一位大神,都需要使用金木水火土五种五行神石来摆一个特定的大阵。而在古籍中,记载是这样的:
kkksc03 大神召唤方法:
金神石的块数必须是 6 的倍数。
木神石最多用 9 块。
水神石最多用 5 块。
火神石的块数必须是 4 的倍数。
土神石最多用 7 块。
lzn 大神召唤方法:
金神石的块数必须是 2 的倍数。
木神石最多用 1 块。
水神石的块数必须是 8 的倍数。
火神石的块数必须是 10 的倍数。
土神石最多用 3 块。
现在是公元 1999 年 12 月 31 日,小 a 和 uim 从 00:00:00 开始找,一直找到 23:00:00,终于,还是没找到神石。不过,他们在回到家后在自家地窖里发现了一些奇怪的东西,一查古籍,哎呦妈呀,怎么不早点来呢?这里有一些混沌之石,可以通过敲击而衰变成五行神石。于是,他们拼命地敲,终于敲出了n块神石,在 23:59:59 完成了两座大阵。然而,kkksc03 大神和 lzn 大神确实出现了,但是由于能量不够,无法发挥神力。只有把所有用 n 块神石可能摆出的大阵都摆出来,才能给他们充满能量。这下小 a 和 uim 傻了眼了,赶快联系上了你,让你帮忙算一下,一共有多少种大阵。
(吐槽一下,这个题面这么长,但其实题意还表达得不是很清楚……)
要把十种召唤方法的母函数乘起来,直接做是比较困难的,但可以转化为分式:
把这些分式全部乘起来,就是 。
高数里学过幂级数展开 ,用 替换 ,再带进 ,得第 项系数为 ,注意 被抵消了。我们这便得到了最后的式子,只不过本题需要用高精度计算。