母函数的理解和应用

母函数的概念

提到母函数,可能大部分人对这个概念会感到十分陌生,这里我们先给大家一段概念性的解释。
母函数就是一列用来展示一串数字的挂衣架。
——赫伯特·唯尔夫 [1] 。
定义:
对于任意数列a0,a1,a2…an 即用如下方法与一个函数联系起来:
在这里插入图片描述

则称G(x)是数列的生成函数(generating function)
其一般形式为:
在这里插入图片描述
可能看到这里大家还不能理解他的意义究竟是怎样的,没关系,我们先给大家一个例题展示一下母函数的具体应用应该是怎样的。大部分时候,我们都用它来解决一些组合问题。

母函数的简单理解

首先考虑一个简单的问题,若有1克、2克、3克、4克的砝码各一 枚,能称出哪几种重量?各有几种可能方案?
如果用母函数的方法解决,我们需要先分别构建出四个砝码对映的母函数
1个1克的砝码可以用函数1+x表示,
1个2克的砝码可以用函数1+x^2表示,
1个3克的砝码可以用函数1+x^3表示,
1个4克的砝码可以用函数1+x^4表示,
这里理解起来也很容易,我们利用多项式乘法中幂次相乘等同于相加的原理,可以枚举出所有的情况。
(1+x)(1+x^ 2)(1+x ^3)(1+x ^4)
=(1+x+x^ 2+x^ 3)(1+x^ 3+x^ 4+x^7)
=1+x+x ^2+2x ^3+2x ^4+2x ^5+2x ^6+2x ^7+x ^8+x ^9+x ^10

很明显,x的幂次就是组成的重量,而x的系数对应的就是有几种方式可以组合出这种重量,读到这里大家可能还是觉得母函数很鸡肋,让我们把问题稍微变复杂一点,
若有1克、2克、3克、4克的砝码各n枚,能称出哪几种重量?各有几种可能方案?
这里的砝码数量变成了n,那么我们对应构造的母函数就应该变成
1个砝码对应的母函数就是(1+x^1 +x^2 +…+x^n)
其中x的1次方就代表选1个1g砝码x的平方就代表选两个1g砝码,x的n次方代表选n个1克砝码
其他砝码对应的母函数也是同理,我们只需要把他们相乘展开,就能得到所有可以可以组合出的重量和每种重量组合方案的方案数。
理解了母函数的意义后接下来的操作就很简单了,我们只需要把多项式乘开就行了,这里我们只需要先把第一个括号和第二个括号相乘形成一个新的括号,在让新括号和第三个括号相乘
这里我们给出一个母函数经典题目作为例子
hdu1028
整数划分问题是将一个正整数n拆成一组数连加并等于n的形式,且这组数中的最大加数不大于n。
这个问题对应的母函数如下图所示。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int maxn=10010;
int n,c1[maxn],c2[maxn];//c1存储每次进行运算前的第一个括号,c2存储临时括号
int main()
{
	while(cin>>n)
	{
		for(int i=0;i<=n;i++)//初始化母函数
			c1[i]=1,c2[i]=0;
		for(int i=2;i<=n;i++)//一共n个括号,运算n-1次
		{
			for(int j=0;j<=n;j++)//j代表了当前第一个括号的所有可能存在的系数
				for(int k=0;j+k<=n;k+=i)//k代表当前正在乘的括号可选择的系数
				c2[j+k]+=c1[j];
			for(int j=0;j<=n;j++)//把c2转移形成新的c1,c2清空
			{
				c1[j]=c2[j];
				c2[j]=0;
			}
		}
		cout<<c1[n]<<endl;
	}
	return 0;
}

具体的运算方法可以参考代码理解

这道题的模板也是大部分母函数问题的模板,在基础上做一点改动就可以解决很多组合问题,当然,整数划分问题也可以用dp的方法解决,感兴趣的同学可以自己思考,在此不多赘述。

了解了母函数的原理和代码写法后,让我们做几道题来巩固一下知识

例题

一.hdu1398

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.
题目意思是,你有1,4,9…直到17的平方种大小的硬币,给你一个数n.问有几种组合方案
这道题的母函数应该是在这里插入图片描述
不难发现,其实代码中需要做的改动很少,只需要对k值每次的变化做出一点变化即可。

#include<bits/stdc++.h>
using namespace std;
const int maxn=305;
int n,c1[maxn],c2[maxn];
int elem[20];
int main()
{
    for(int i=1;i<=17;i++)//打表求出每个母函数变化的频率
        elem[i]=i*i;
    while(cin>>n)
    {
        if(!n)break;
        for(int i=0;i<=n;i++)
            c1[i]=1,c2[i]=0;
        for(int i=2;i<=17;i++)
        {
            for(int j=0;j<=n;j++)
            for(int k=0;k+j<=n;k+=elem[i])//修改关键在此
                c2[j+k]+=c1[j];
            for(int i=0;i<=n;i++)
                c1[i]=c2[i],c2[i]=0;
        }
        cout<<c1[n]<<endl;
    }
}

二.hdu1085

给定一些中国硬币(1、2、5)及其编号分别为num_1、num_2和num_5,请输出您无法用给定硬币支付的最低值。
你,ACMer,应该很容易解决问题。
这道题目基本没有什么变化,输出的时候从头枚举,输出第一次为0的地方即可
有一点需要提到的变化是,j的取值sum需要每次括号乘完后更新。因为每个母函数中对应的个数是确定的,所以枚举条件也发生了一点小变化。

#include<bits/stdc++.h>
using namespace std;
const int maxn=10010;
int num[4],sum,c1[maxn],c2[maxn];
int elem[4]={0,1,2,5};
int main()
{
    while(~scanf("%d%d%d",&num[1],&num[2],&num[3]))
    {
        if (num[1]==0&&num[2]==0&&num[3]==0)return 0;
        sum=num[1];
        memset(c1,0,sizeof(c1));
        memset(c2,0,sizeof(c2));
        for(int i=0;i<=num[1];i++)
            c1[i]=1,c2[i]=0;
        for(int i=2;i<=3;i++)
        {
            for(int j=0;j<=sum;j++)
            for(int k=0;k<=elem[i]*num[i];k+=elem[i])
            c2[j+k]+=c1[j];
            sum+=elem[i]*num[i];
            for(int j=0;j<=sum;j++)
            c1[j]=c2[j],c2[j]=0;
        }
        for(int i=1;i<=sum+1;i++)
        if(!c1[i]){
            printf("%d\n",i);
            break;
        }
    }
    return 0;
}

三.hdu1171

题目大意是,一共有n种物品,告诉你每种物品的价值和数量,尽可能公平的把他们分成两部分,并且第一部分不能小于第二部分。
我们仍然可以先套用母函数的模板求出所有可以组合出的分类方法,然后从总价值的一半开始向下枚举,第一个可以达到的方案就是最公平的方案,让第一部分大于一半即可。

#include <bits/stdc++.h>
using namespace std;
const int maxn=300005;
int n,c1[maxn],c2[maxn],num[100];
int elem[100];
int main() {
    while (~scanf("%d",&n)){
        if (n<0)return 0;
        for (int i=1;i<=n;i++){
            scanf("%d%d",&elem[i],&num[i]);
        }
        memset(c1,0,sizeof(c1));//初始化
        memset(c2,0,sizeof(c2));
        for (int i=0;i<=num[1];i++)//初始化第一个母函数
            c1[i*elem[i]]=1;
        int sum=0;
        for (int i=1;i<=n;i++)sum+=elem[i]*num[i];//计算上限
    	for (int i=2;i<=n;i++){
    		for (int j=0;j<=sum/2;j++){
                if (c1[j]){//如果系数不为0才加,小的优化。
        			for (int k=0;k+j<=sum/2&&k<=elem[i]*num[i];k+=elem[i])
        				c2[j+k]+=c1[j];
                }
            }
    		for (int j=0;j<=sum/2;j++){
    			c1[j]=c2[j];
    			c2[j]=0;
    		}
    	}
    	int ans=0;
        for (int i=sum/2;i>=0;i--){//从中间开始枚举排列组合,最接近中间的方法就是解法。
            if (c1[i]){ans=i;break;}
        }
        printf("%d %d\n",sum-ans,ans);
    }
    return 0;
}

四.hdu1709

有一个天平,告诉你有n个砝码,并且知道每种砝码的质量,每种砝码都只有一个,可以把砝码放在天平的另一边,问那些可以被称出来。
我们思考一个问题,相当与母函数多了1个负一次方的项,但是这样的话,我们很难知道所有可以组成的值,这时候我们观察一下枚举过程的的性质,发现其实变化的地方只有一个,就是我们不光可以获得j+k项的组合,也可以获得|k-j|的重量所以我们只需要加一句c2[abs(k-j)]+=c1[j]即可。

#include <bits/stdc++.h>
using namespace std;
const int maxn=100005;
int n,c1[maxn],c2[maxn],num[105];
int elem[105];
int main() {
    while (~scanf("%d",&n)){
        for (int i=1;i<=n;i++)scanf("%d",&elem[i]),num[i]=1;
        int sum=0;
    	for (int i=1;i<=n;i++)sum+=elem[i]*num[i];//num全为1,此处为了模板好修改,好理解。
        memset(c1,0,sizeof(c1));
        memset(c2,0,sizeof(c2));
    	c1[0]=c1[elem[1]]=1;
    	for (int i=2;i<=n;i++){
    		for (int j=0;j<=sum;j++)
    			for (int k=0;k+j<=sum&&k<=elem[i];k+=elem[i])
    			{
    				c2[abs(k-j)]+=c1[j];//天平问题唯一的区别就是可以组合出k和j的绝对值,加上即可。
    				c2[j+k]+=c1[j];
    			}
    		for (int j=0;j<=sum;j++){
    			c1[j]=c2[j];
    			c2[j]=0;
    		}
    	}
    	vector<int>ans;
    	for (int i=0;i<=sum;i++)
    		if (!c1[i])ans.push_back(i);
    	printf("%lu\n",ans.size());
    	for (int i=0;i<ans.size();i++)
    		printf("%d%c",ans[i],i==ans.size()-1?'\n':' ');
    }
    return 0;
}

五.hdu1012

转眼到了收获的季节,由于有TT的专业指导,Lele获得了大丰收。特别是水果,Lele一共种了N种水果,有苹果,梨子,香蕉,西瓜……不但味道好吃,样子更是好看。
于是,很多人们慕名而来,找Lele买水果。
甚至连大名鼎鼎的HDU ACM总教头 lcy 也来了。lcy抛出一打百元大钞,“我要买由M个水果组成的水果拼盘,不过我有个小小的要求,对于每种水果,个数上我有限制,既不能少于某个特定值,也不能大于某个特定值。而且我不要两份一样的拼盘。你随意搭配,你能组出多少种不同的方案,我就买多少份!”
现在就请你帮帮Lele,帮他算一算到底能够卖出多少份水果拼盘给lcy了。
注意,水果是以个为基本单位,不能够再分。对于两种方案,如果各种水果的数目都相同,则认为这两种方案是相同的。
最终Lele拿了这笔钱,又可以继续他的学业了~
最基本的板子,只是限制了每一种水果的范围。

#include <bits/stdc++.h>
using namespace std;
const int maxn=100005;
int n,m,c1[maxn],c2[maxn],l[maxn],r[maxn];
int main() {
    while (~scanf("%d%d",&n,&m)){
        for (int i=1;i<=n;i++)scanf("%d%d",&l[i],&r[i]);
        memset(c1,0,sizeof(c1));
        memset(c2,0,sizeof(c2));
        for (int i=l[1];i<=r[1];i++)c1[i]=1;
        for (int i=2;i<=n;i++){
            for (int j=0;j<=m;j++)
                for (int k=l[i];k+j<=m&&k<=r[i];k++)
                    c2[j+k]+=c1[j];
            for (int j=0;j<=m;j++)
                c1[j]=c2[j],c2[j]=0;
        }
        printf("%d\n",c1[m]);
    }
    return 0;
}

关于基础型母函数的大部分变化都已经可以涵盖在这些题目中了,当然普通母函数只能解决这些简单的组合问题,一些排列问题和特殊的生成函数问题,还需要用指数型母函数等更多的技巧才能解决。大家可以自行学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值