母函数和排列组合

generating function and permutation&combination

Posted by BY on September 26, 2018

前言

出来混的总是要还的

正文

问题来源

看博客的时候看到的,是个基础的算法,本应该早点掌握的。

问题描述

在谈论母函数问题之前,我们先看一个简单的问题描述:假如有两组数据(A,B)和(C,D),每组中选出一个构成一个组合,总共有几种选法?很显然总共有4种选法:AC,AD,BC,BD。而且很容易联想到这个式子(A+B)(C+D)=AC+AD+BC+BD。式子中的几个乘积项就是上面的4种选法。假如把问题换一下:每组中选出一个或0个数据构成组合,总共有几种组合?那么结果就变成:{空},A,B,C,D,AC,AD,BC,BD,而式子(1+A+B)(1+C+D)=1+C+D+A+AC+AD+B+BC+BD,正好和上面组合的结果又一致(1代表什么都没选)。从这2个例子我们可以发现多项式乘积和组合存在着某种关系。事实上我们可以这么理解:(1+A+B)可以理解为从第一组数据中取0个数据,取A或者取B,同样(1+C+D)可以理解为从第二组数据取0个数据,取C或者取D。两者相乘的结果就表示了所有的组合。再看一下这个多项式:

(1+x)*(1+x+x^2)*(1+x^3)=1+2x+2x^2+2x^3+2x^4+2x^5+x^6 

这个多项式和上面的有一些区别了,它的幂级数超过1了。如果要从(1+x)、(1+x+x^2)和(1+x^3)中得到x的2次方的话,有两种选择:从(1+x) 和(1+x+x^2)中分别选择一个x或者从(1+x+x^2)中选择x^2;如果要得到x的6次方的话,只有1种选择,就是从(1+x) 中选择x、(1+x+x^2)中选择x^2、(1+x^3)中选择x^3。也就是说乘积结果的每一项an * x^n的前面的系数an表示了从(1+x)、(1+x+x^2)和(1+x^3)中得到x^n的组合数。
  其实上面的例子就利用了母函数的思想,下面来具体讨论一下母函数。

一.什么是母函数

  下面这个对于母函数的描述摘自维基百科:   在数学中,某个序列 的母函数是一种形式幂级数,其每一项的系数可以提供关于这个序列的信息。
  也就是说母函数是针对某个序列的,它的外在表现形式是一种形式幂级数。比如说有这样一个序列a0,a1,……an,构造一个函数
  f(x)=a0+a1x+a2x^2+……+anx^n
  则f(x)是序列a0,a1,……an的母函数。比如说最常见的(1+x)n,它是序列C(n,0),C(n,1),C(n,2)…C(n,n)的母函数。
  母函数包括几种,其中最常见的是普通型母函数和指数型母函数。普通型母函数是形如 f(x)=a0+a1x+a2x^2+……+anx^n的函数,而指数型母函数是形如G(x) = a0 + a1*(x)/1! + a2*(x^2)/ 2! + a3*(x^3)/3! + …… an*(x^n)/k!的函数。

二.利用普通型母函数解决组合问题

 利用母函数的思想可以解决很多组合问题,下面举例说明:
 1.口袋中有白球2个,红球3个,黄球1个,从袋中摸出3个球有几种取法?
  和上面描述的例子类似,我们可以用次数代表球的个数,多项式的每一项前面的系数代表取法的种树。
  可以很容易地写出下面这个式子:
(1+x+x^2)(1+x+x^2+x^3)(1+x)
  (1+x+x^2)表示有白球2个,1表示白球不取,x代表取1个白球,x2代表取2个白球,即用x的次数表示取球的个数,后面的也是类似。那么这个多项式的乘积就把所有的情况都表示出来了,对于最终乘积的每一项anxn,表示取n个球有an种取法。
2.有若干个1克,2克,5克的砝码,要称出20克的重量,有多少种称法?
  这里不限制砝码的个数,无所谓,照样写出母函数:
(1+x+x^2+x^3+……x^k+….)(1+x^2+x^4+x^6……+x^2n+……)(1+x^5+x^10+……x^5m+……)
  因为要称出20克,所以只需要找找到结果中次数为20 的那一项就可以得到结果。
3.同样对于正数划分也可以解决,比如有整数20,划分成1,2,5,有多少种划分方法?
  解法和2的类似。
还有一类题目和这类似,有n个球放到m个盒子中,有多少种不同的放法?
(1+x+x^2+x^3+…x^k+…)(1+x+x^2+x^3+…x^k+…)(1+x+x^2+x^3+…x^k+…)总共有m项,然后找出乘积中次数为n的那一项系数。

三.利用指数型母函数解决排列问题

  1.口袋中有白球2个,红球3个,黄球1个,任取3个作为一个排列,总共有多少种排列?
  类似地用指数型母函数解决
  用(1+x/1!+x^2/2!)表示取白球0个,1个或者2个
  那么(1+x/1!+x^2/2!)(1+x/1!+x^2/2!+x^3/3!)(1+x/1!)来表示所有的排列结果。
  =1+3x+4x^2+19x^3/6+19x^4/12+6x^5/12+x^6/12
  =1+3(x/1!)+8(x^2/2!)+19(x^3/3!)+38(x^4/4!)+60(x^5/5!)+60(x^6/6!)
  找到次数为3的那一项,系数为19,那么总共有19种排列。
  2.用1,2,3,4能够组成多少个5位数,要求1出现2次或者3次,2出现0次或者1次,3没有限制,4只出现偶数次。
  (x^2/2!+x^3/3!)(1+x)(1+x/1!+x^2/2!+x^3/3!+…..x^k/k!+….)(1+x^2/2!+x^4/4!+……+x^2n/(2n)!+……)
   每个式子的含义就不多解释了,读者应该能看懂它的含义。最终的结果就是x^5/5!这一项的系数。
  用代码去实现母函数的计算过程很简单,它是模拟我们人工计算多项式乘积的过程,比如有多项式H1H2H3……
  我们先计算H1和H2的乘积,得到结果H’,再用H’和H3相乘……依次类推下去,直到得到最终的结果。

例题:

整数拆分 输入一个整数n,把它拆成若干个整数相加,输出有多少种拆分的方法。
比如:4=3+1=2+2=2+1+1=1+1+1+1,有5种。
解:
所谓整数拆分即把整数分解成若干整数的和(相当于把n个无区别的球放到n个无标志的盒子,盒子允许空,也允许放多于一个球)。
G(x)=(1+x+x^2+…)(1+x^2+x^4+…)(1+x^3+x^6+…)…

#include <stdio.h>
#include <string.h>
int main()
{
    int c1[200],c2[200];     //c1保存当前表达式里各项的系数,c2是中间量
    int n,i,j,k;
    while(scanf("%d",&n)!=EOF)  
    {
        for(i=0;i<=n;i++)   //n拆分的数必定小于或等于n,所以只用从0遍历到n,即母函数的第1个括号里的表达式只能是1+x+x^2+…+x^n
        {
            c1[i]=1;          //初始化c1[],c1[]的下标是项的指数,值是项的系数
            c2[i]=0;
        }
        for(i=2;i<=n;i++)       //后面有n-1个表达式(括号里的式子),要展开n-1次,i指第i个表达式
        {
            for(j=0;j<=n;j++)
            {
                for (k=0;k+j<=n;k=k+i)
                {
                    c2[k+j]+=c1[j];        //只用保留指数<=n的项,所以k+j<=n
                }                        //j指当前表达式里项的第j个项,k指后一个表达式里项的指数,k=k+i
            }                            //两个相邻表达式展开
            for(j=0;j<=n;j++)
            {
                c1[j]=c2[j];
                c2[j]=0;
            }            //每展开一次,就将c1[]更新,c2[]归0
        }
        printf("%d\n",c1[n]);
    }
    return 0;
}

钱币组合:
有面值为1,2,5的3种硬币,输入各硬币的个数,求最小的一个不能组合的成的价值。
比如输入1 1 3,输出4
解:
G(x)=(1+x+x^2+…)(1+x^2+x^4+…)(1+x^5+x^10+…)
3种面值,所以只有3个括号表达式。

#include <stdio.h>
#include <string.h>
int main()
{
    int a,b,c,i,j,k,sum;
    int c1[8008],c2[8008];
    int elem[3]={1,2,5};
    int num[4];
    while(scanf("%d%d%d",&a,&b,&c)!=EOF)
    {
        if(a==0&&b==0&&c==0)
            break;
        sum=1*a+2*b+5*c;
        num[1]=a;
        num[2]=b;
        num[3]=c;     //num[]存放括号内表达式的项的个数
 
        memset(c1,0,sizeof(c1));
        memset(c2,0,sizeof(c2));     //c1[],c2[]全部归0
        for(i=0;i<=a;i=i+elem[0])    //初始化第1个表达式里各项的系数
            c1[i]=1;
    
        for(i=2;i<=3;i++)
        {
            for(j=0;j<=sum;j++)
            {
                for(k=0;k<=num[i]*elem[i-1]&&k+j<=sum;k=k+elem[i-1])
                {
                    c2[j+k]+=c1[j];
                }
            }
            for(j=0;j<=sum+1;j++)
            {
                c1[j]=c2[j];
                c2[j]=0;
            }
        }
        
        for(i=0;i<=sum+1;i++)
        {
            if(c1[i]==0)
            {
                printf("%d\n",i);
                break;
            }
        }
    }
    return 0;
}

总结:

咱们赶快趁热打铁,来几道题目:
(相应题目解析均在相应的代码里分析)
1.  题目:http://acm.hdu.edu.cn/showproblem.php?pid=1028
代码:http://www.wutianqi.com/?p=587
这题大家看看简单不?把上面的模板理解了,这题就是小Case!
看看这题: 2.  题目:http://acm.hdu.edu.cn/showproblem.php?pid=1398
代码:http://www.wutianqi.com/?p=590
要说和前一题的区别,就只需要改2个地方。 在i遍历表达式时(可以参考我的资料—《母函数详解》),把i<=nNum改成了ii<=nNum,其次在k遍历指数时把k+=i变成了k+=ii; Ok,说来说去还是套模板~~~
3.  题目:http://acm.hdu.edu.cn/showproblem.php?pid=1085
代码:http://www.wutianqi.com/?p=592
这题终于变化了一点,但是万变不离其中。
大家好好分析下,结合代码就会懂了。
4.  题目:http://acm.hdu.edu.cn/showproblem.php?pid=1171
代码:http://www.wutianqi.com/?p=594
还有一些题目,大家有时间自己做做:
HDOJ:1709,1028、1709、1085、1171、1398、2069、2152
附:
1.在维基百科里讲到了普通母函數、指數母函數、L級數、貝爾級數和狄利克雷級數:
http://zh.wikipedia.org/zh-tw/%E6%AF%8D%E5%87%BD%E6%95%B0
2.Matrix67大牛那有篇文章:什么是生成函数:
http://www.matrix67.com/blog/archives/120
3.大家可以看看杭电的ACM课件的母函数那篇,我这里的图片以及一些内容都引至那。

结语

查漏补缺。