我们先来看一下一个简单的问题:
例1:
小明出门旅游,需要带一些食物,包括薯片,巧克力,矿泉水,汉堡,牛奶和糖果。
经过估计,他觉得带n(n<10^100)件食物比较合适,但他还有一些癖好:
.最多带 1 个汉堡
.巧克力的块数是 5 的倍数
.最多带 4 瓶矿泉水
.薯片的包数是一个偶数
.最多带 3 罐牛奶
.糖果的个数是 4 的倍数
问你小明有多少种方式来准备这次旅行所带的食物。
如果不看数据范围,那么处理本题一般的思路是首先考虑 1,3,5 三个条件,可以用枚举来解决,之后问题转化成了一个不定方程非负整数解的问题:
首先想到的方法是用
表示
非负整数解的个数,把 3 个变量分开来考虑做 3 次时间复杂为
的递推。
但是,天哪,n太大了,即使是
也会炸掉,那么怎么做呢?
我们新定义一个无穷序列的幂级数,一般来说母函数有形式:
我们称
是序列
的母函数
那么这样做有什么好处呢?
我们这样做可以将件数的意义转到x的幂上面,我们可以用幂代表件数,系数代表方案数
我们可以在做系数乘法的同时做幂的加法,方案数可以用两种不重合的方案相乘得到,但是物品数是相加,由于
所以a种方案取A物品A个,b种方案取B物品B个,合起来就是ab种方案取AB物品A+B个,用
代表不带,那么之前的问题就可以这样考虑了:
最多带 1 个汉堡即
(带或不带)
巧克力的块数是 5 的倍数即
(五的倍数)
最多带 4 瓶矿泉水即
薯片的包数是一个偶数即
最多带 3 罐牛奶即
糖果的个数是 4 的倍数即
但是问题出现了,我们没有办法计算无限项,即使是只算n项这个算法也并不比之前的优秀,但是我们可以用一点简单的高中知识即可知道
这里的x没有说多少,又由于当n趋向于正无穷时
的数,它的
因此,我们可以在这里耍流氓假设
,那么
因此
最多带 1 个汉堡
巧克力的块数是 5 的倍数
最多带 4 瓶矿泉水即
薯片的包数是一个偶数即
最多带 3 罐牛奶即
糖果的个数是 4 的倍数即
乘起来得:
巧了,只剩一个分母了我们来看看它等于什么,因为
而
这咋办?完了吗?
并不,我们可以来看看通项
很明显,
中
的系数的值等价于方程
的非负整数解的个数,用隔板法易知系数
所以上面那个问题的答案
这样我们运用母函数的方法解决了本问题,时间复杂度仅为
,但是在 n 比较大的时候需要使用高精度乘法。
代码:(由于太丑了,尽作参考意义)
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=2005;
char s[MAXN];
int a[3][MAXN],b[MAXN],c[MAXN],ans[MAXN];
void cnf(int x,int y)
{
memset(b,0,sizeof(b));
for(int i=0;i<MAXN;i++)
{
for(int j=0;j<MAXN;j++)
{
b[i+j]+=a[x][i]*a[y][j];
}
}
for(int i=0;i<MAXN;i++)
{
b[i+1]+=b[i]/10;
b[i]%=10;
}
for(int i=0;i<MAXN;i++)
{
a[x][i]=b[i];
}
}
int main()
{
scanf("%s",s);
int len=strlen(s);
for(int i=0;i<len/2;i++)
{
swap(s[i],s[len-i-1]);
}
for(int i=0;i<len;i++)
{
a[2][i]=a[1][i]=a[0][i]=s[i]-'0';
}
a[1][0]+=1;
a[2][0]+=2;
for(int i=0;i<=len;i++)
{
a[1][i+1]+=a[1][i]/10;
a[1][i]%=10;
a[2][i+1]+=a[2][i]/10;
a[2][i]%=10;
}
cnf(0,1);
cnf(0,2);
for(int i=0;i<MAXN;i++)
c[i]=a[0][i];
for(int i=MAXN-2;i>=0;i--)
{
int k=c[i+1]*10+c[i];
ans[i]=k/6;
k%=6;
c[i+1]=0;
c[i]=k;
}
for(int i=MAXN-5;i>=0;i--)
{
int k=ans[i+5]*100000+ans[i+4]*10000+ans[i+3]*1000+ans[i+2]*100+ans[i+1]*10+ans[i];
k%=10007;
ans[i+5]=k/100000%10;
ans[i+4]=k/10000%10;
ans[i+3]=k/1000%10;
ans[i+2]=k/100%10;
ans[i+1]=k/10%10;
ans[i]=k%10;
}
int key=0;
for(int i=6;i>=0;i--)
{
if(ans[i])
key=1;
if(key)
printf("%d",ans[i]);
}
printf("\n");
}
例2:
Fruit HDU - 2152
一句话题意:有
种水果,每种必须选
到
个,要选
个,问方案数。
(注意:水果是以个为基本单位,不能够再分。对于两种方案,如果各种水果的数目都相同,则认为这两种方案是相同的。)
这个很简单,有了刚刚的基础之后,我们可以把每个
乘起来,最后
的系数便是答案。
代码如下:
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=10005;
int n,m,c1[MAXN],c2[MAXN],a[MAXN],b[MAXN];
int main()
{
while(~scanf("%d %d",&n,&m))
{
for(int i=1;i<=n;i++)
{
scanf("%d %d",&a[i],&b[i]);
}
for(int i=0;i<=m;i++)
{
c1[i]=0;
c2[i]=0;
}
for(int i=a[1];i<=b[1];i++)
c1[i]=1;
for(int i=2;i<=n;i++)
{
for(int j=a[1];j<=m;j++)
{
for(int k=a[i];k+j<=m&&k<=b[i];k++)
{
c2[j+k]+=c1[j];
}
}
for(int j=a[1];j<=m;j++)
{
c1[j]=c2[j];
c2[j]=0;
}
}
printf("%d\n",c1[m]);
}
}
到了这里,增加一个小插曲,总结一下基本操作,其实除了上面的操作以外还有其他的,比如:
1.放缩:
说白了就是全部乘上c;
2.加减法
下面将讲一下指数型函数:
例3:
ACM/ICPC Regional Contest Beijing 2002 Problem F Chocolate
一个口袋中装有巧克力,巧克力的颜色有 c 种。现从口袋中取出一个巧克力,若取出的
巧克力与桌上已有巧克力颜色相同,则将两个巧克力都取走,否则将取出的巧克力放在桌上。
设从口袋中取出每种颜色的巧克力的概率均等。求取出 n 个巧克力后桌面上剩余 m 个巧克
力的概率。
很明显只有
的时候才是有解的,本题容易想到的方法是递推,用
表示取出块巧克力后,桌面上剩余块巧克力的概率,然后通过
进行递推求解。这种方法的时间复杂度为
,在题目中所给的数据范围下很容易超时。
我们再来考虑考虑用母函数来解决本问题:
我们来求概率的方法是把所有状况分成若干个等概率的状况,看所求的占了多少种状况。由于题目中说每次从口袋中取出每种颜色的巧克力概率相等,所以总状况按照 n 次取的巧克力分为
种状况,这种分法是考虑到不同颜色巧克力之间的排列关系的,所以要在每种的下面除以
。
比如这样:
这就是指数型母函数。
剩余 m 个巧克力这个条件我们可以转化为 c 种巧克力中有 m种巧克力取出了奇数次,c-m
种巧克力取出了偶数次.我们把巧克力分成一种一种考虑,那么那些取出了奇数次的巧克力对应的指数型母函数应该是奇数幂次的系数为 1,偶数幂次的系数幂次为 0。
那么就是
,同样的道理,那些取出了偶数次的巧克力对应
的指数型母函数为
.
所以只要知道
和
的值就行了,
我们现在来研究一下指数型母函数的性质。
我们称:
为序列
的指数型母函数。
我们先来研究一下最基本的序列
所对应的指数型母函数,
它对应的指数型母函数根据定义为:
然而有大佬成功想出了它的值是
,要用泰勒公式,泰勒公式具体怎么证我就不在这里证了,要看证明的去看百度。
因此
取
,得
(
)同理可证,运用泰勒公式,我们可以得到:
序列
的指数型母函数为
.
序列
的指数型母函数为
.
序列
的指数型母函数为
.
因此对应
种巧克力取出了奇数次,
种巧克力取出了偶数次的指数型母函数为
现在我们可以开始展开这个式子了,我们可以把
看作一个整体,将
展开成关于
的多项式的形式,我们考虑展开后每一项
(不妨先设
同理)中
的系数为
,把它们加起来即可。
这样,我们可以先把
用
的时间展开成
的多项式,设
的系数为
,那么所求概率为:
因此,这个问题我们可以在
的复杂度内解决。
附一下代码:
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=105;
double C[MAXN][MAXN];
int c,n,m;
double ksm(double x,long long y)
{
double k=1;
while(y)
{
if(y&1)
k=(k*x);
x=(x*x);
y>>=1;
}
return k;
}
void Init()
{
C[0][0]=1;
for(int i=1;i<=100;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
}
int main()
{
Init();
while(~scanf("%d %d %d",&c,&n,&m)&&c)
{
if((n-m)&1||n<m||c<m)
{
printf("0.000\n");
continue;
}
double ans=0.0;
for(int i=0;i<=m;i++)
{
for(int j=0;j<=c-m;j++)
{
double k=2.0*(i+j)-c;
double jans=ksm(k/(1.0*c),n)*C[m][i]*C[c-m][j];
if((m-i)&1)
ans-=jans;
else
ans+=jans;
}
}
ans/=ksm(2.0,c);
ans*=C[c][m];
printf("%.3f\n",ans);
}
}
例4:
HDU 2065 “红色病毒”问题
由于B、D可出现0-n之间任意次,A、C只能出现0-n之间偶数次,
在有了之前的基础后,我相信这个问题并不难。结合指数型母函数可得:
所以,第
项的系数为
由于题目要输出末两位,因此,
这个是代码:
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const long long MOD=100;
long long n;
long long ksm(long long x,long long y)
{
long long k=1;
while(y)
{
if(y&1)
k=(k*x)%MOD;
x=(x*x)%MOD;
y>>=1;
}
return k;
}
int main()
{
long long T;
while(~scanf("%lld",&T)&&T)
{
for(long long t=1;t<=T;t++)
{
scanf("%lld",&n);
printf("Case %lld: %lld\n",t,(ksm(4,n-1)+ksm(2,n-1))%MOD);
}
printf("\n");
}
}