算法学习笔记:母函数详解
引言
母函数(Generating function,生成函数)是组合数学中一种重要的方法,这里只对最简单的普通母函数作简单介绍。其主要思想是,把离散序列和幂级数对应起来。
先来看一个最经典的例子:给你1克、2克、3克、4克的砝码各一枚,问称出1~10克的方案分别有多少种?
用母函数的方法,只需要算一个式子就好了:
n次项表示称出n克的方案,例如当n=7时有两种方案(3+4与1+2+4)。
当我们把两个幂级数相乘时,我们相当于把两种状态的砝码拿到了一起:
于是,我们把看上去有点困难的组合学问题转化为了代数问题。
我们来看例题:
(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克?
大部分题解讲到这里给出的程序,乍一看跟上面讲的母函数关系不大,倒有点像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 傻了眼了,赶快联系上了你,让你帮忙算一下,一共有多少种大阵。
(吐槽一下,这个题面这么长,但其实题意还表达得不是很清楚……)
要把十种召唤方法的母函数乘起来,直接做是比较困难的,但可以转化为分式:
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
· 5. Nginx 负载均衡配置案例(附有详细截图说明++)