母函数(普通型、指数型)

先从几个简单的问题开始(普通型):

问题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;
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值