先从几个简单的问题开始(普通型):
问题1:有面值为1元、2元、3元的纸币各一张,要凑出3元,有几种取法?
很容易就能得出是2种取法。
如果用x的指数表示面值,可以得到母函数G(x)=(1+x)(1+x2)(1+x3)
将其展开得:G(x)=1+x+x2+2x3+x4+x5+x6
x3前面的系数代表了凑出面值为3的方案数。
解释: 因为多项式展开时的运算类似于排列组合,并且两个项相乘其实就是x指数的相加,所以最终可以用系数来表示方案数。这就是普通型母函数。
关于括号里为什么是1+xn的形式:1表示某种面值的纸币不取。(可以看成x0,取一张面值为0的纸币)。
变式1:如果纸币的数量无限,有几种取法?
这时的母函数可以表示为:G(x)=(1+x+x2+…+xn)(1+x2+x4+…+x2n)(1+x3+x6+…+x3n)
解释: 第一个括号表示面值为1元的纸币的取法,第二个括号表示2元的,第三个表示3元的。因为展开后的一个项是从三个括号里面各取一项进行相乘,所以用括号里x的系数来表示某个面值的纸币取的数量×面值。
题目要求面值和是3元,因此只要展开到x3的项即可,后面的可以不用去管。
展开后的G(x)=1+x+2x2+3x3+…
此时x3前面的系数是3,表示有三种取法。
变式2:如果面值为1元、2元、3元的纸币分别为3,2,1张,有几种取法?
跟上面做法类似,令G(x)=(1+x+x2+x3)(1+x2+x4)(1+x3)
因为是有限张,所以括号内的项数也是有限的,其余做法略。
问题2:有一个字符串:aaabbccc,从字符串内取出4个字符,组成一个新的字符串,有几种取法(要求按字母表顺序排列)?
和问题1类似,但是结果只对取出字母的数量有要求,而问题一对纸币数量无要求,只对面值和有要求。
因此此题的母函数可以写成:G(x)=(1+x+x2+x3)(1+x+x2)(1+x+x2+x3)
展开后得:G(x)=1+3x+6x2+9x3+10x4+9x5+6x6+3x7+x8
解释: 括号内的x指数表示字母出现的次数,从左到右的括号分别为abc。展开后项的指数就表示新的字符串长度。x4的系数为10,因此有10种取法。
普通母函数大致介绍完了,那么如何用代码来实现函数的展开呢?
手工计算的做法是先将前两个括号相乘,得到一个新的括号,再将新的括号与后面的括号依次相乘。代码的实现也是同样的做法,只不过省略了一些步骤,比如最终结果是要求x5前面的系数,那么计算的时候算到x5过就行,后面的计算全部跳过,因为对结果不会产生影响了。
以下为问题2的模板:
#include <bits/stdc++.h>
#define MAXN 10
using namespace std;
int c1[MAXN], c2[MAXN]; //c1保存结果,c2存储运算时的中间量
int element[MAXN]; //表示每种字母有几个
//n表示有n个括号(或n种字母),r表示要取出r个字母(或结果计算到指数为r的项就停止)
void get_ans(int n, int r)
{
memset(c1, 0, sizeof(c1));
memset(c2, 0, sizeof(c2));
//将c1初始化为第一个括号的系数
for (int i = 0; i <= element[0]; i++)
c1[i] = 1;
//第一层循环表示第i+1个括号
for (int i = 1; i < n; i++)
{
//第二层循环表示i+1个括号内指数为j的项
for (int j = min(element[i], r); j >= 0; j--)
{
//第三重循环表示前面计算结果中指数为k的项
for (int k = r - j; k >= 0; k--)
{
//合并同类项,注意是加上前面结果的系数
c2[k + j] += c1[k];
}
}
//一个括号计算完后将c2的结果转移至c1
for (int j = r; j >= 0; j--)
{
c1[j] = c2[j];
c2[j] = 0;
}
}
}
int main()
{
element[0] = 3;
element[1] = 2;
element[2] = 3;
get_ans(3, 8);
for (int i = 0; i <= 8; i++) //输出所有项的系数
printf("%d ", c1[i]);
printf("\n");
//输出为:1 3 6 9 10 9 6 3 1 符合结果系数
return 0;
}
以下为变式1的模板:
#include <bits/stdc++.h>
#define MAXN 10
using namespace std;
int c1[MAXN], c2[MAXN]; //c1保存结果,c2存储运算时的中间量
int element[MAXN]; //表示纸币的面值
void get_ans(int n, int r)
{
memset(c1, 0, sizeof(c1));
memset(c2, 0, sizeof(c2));
for (int i = 0; i <= r; i += element[0])
c1[i] = 1;
//第一层循环表示第i+1个括号
for (int i = 1; i < n; i++)
{
//第二层循环表示i+1个括号内指数为j的项(每次加上面值)
for (int j = 0; j <= r; j += element[i])
{
//第三重循环表示前面计算结果中指数为k的项(因为不知道有多少项,要把所有指数都遍历一遍)
for (int k = r - j; k >= 0; k--)
{
//合并同类项,注意是加上前面结果的系数
c2[k + j] += c1[k];
}
}
//一个括号计算完后将c2的结果转移至c1
for (int j = r; j >= 0; j--)
{
c1[j] = c2[j];
c2[j] = 0;
}
}
}
int main()
{
element[0] = 1;
element[1] = 2;
element[2] = 3;
get_ans(3, 3);
for (int i = 0; i <= 3; i++) //输出所有项的系数
printf("%d ", c1[i]);
printf("\n");
//输出为:1 1 2 3 符合结果系数
return 0;
}
如果排列的顺序不同视为不同的取法(指数型):
相比普通型母函数,指数型母函数除了相应指数的阶乘
因为取三个a,如果这三个a如果看成是不一样的,有3!种排列方式。(至于为什么这么做,跟最终结果的取法有关)
那么展开后很显然xn的系数是一个分数,这个分数乘上对于指数的阶乘就是结果数了。因为n个字符有n!中排列方式(算上重复的),而在未展开时除的对应指数的阶乘就是为了消除这种重复,也就解答了上面的问题。
代码跟普通型母函数差不多,只是c1和c2从int类型变成了double类型。
为了加速阶乘运算,可以进行打表,事先将阶乘结果保存在数组里,要算阶乘的时候直接取即可。
以问题2为例,如果对于排列顺序没有要求,那么有几种排列?
//指数型母函数模板
#include <bits/stdc++.h>
#define MAXN 10
using namespace std;
double f[10]; //保存阶乘结果
double c1[MAXN], c2[MAXN]; //c1保存结果,c2存储运算时的中间量
int element[MAXN]; //表示字母数量
void factorial(int n) //阶乘预处理
{
f[0] = 1;
for (int i = 1; i <= n; i++)
{
f[i] = f[i - 1] * i;
}
}
void get_ans(int n, int r)
{
memset(c1, 0, sizeof(c1));
memset(c2, 0, sizeof(c2));
//将c1初始化为第一个括号的系数
for (int i = 0; i <= element[0]; i++)
c1[i] = 1.0 / f[i];
//第一层循环表示第i+1个括号
for (int i = 1; i < n; i++)
{
//第二层循环表示i+1个括号内指数为j的项
for (int j = min(element[i], r); j >= 0; j--)
{
//第三重循环表示前面计算结果中指数为k的项
for (int k = r - j; k >= 0; k--)
{
//合并同类项,注意是加上前面结果的系数
c2[k + j] += c1[k] / f[j];
}
}
//一个括号计算完后将c2的结果转移至c1
for (int j = r; j >= 0; j--)
{
c1[j] = c2[j];
c2[j] = 0;
}
}
}
int main()
{
factorial(10);
element[0] = 3;
element[1] = 2;
element[2] = 3;
int r = 8;
get_ans(3, r);
for (int i = 0; i <= r; i++)
printf("%.0f ", c1[i] * f[i]);
return 0;
}