I am a teacher!

导航

递推(二):递推法的应用

      下面通过一些典型实例及其扩展来讨论递推法的应用。

【例2】骨牌铺方格

      在2×n的一个长方形方格中,用一种2×1的骨牌铺满方格。输入n(n<=40),输出铺放方案的总数。

      例如n=3时,为2×3方格,骨牌的铺放方案有三种,如下图1所示。

 

图1  2×3方格的骨牌铺放方案

      (1)编程思路。

      设f[i]为铺满2*n方格的方案数,则有    f[i]=f[i-1]+f[i-2]。

      其中,f[i-1]为铺满2*(n-1)方格的方案数(既然前面的2*(n-1)的方格已经铺满,那么最后一个只能是竖着放)。f[i-2]为铺满2*(n-2)方格的方案数(如果前面的2*(n-2)的方格已经铺满,那么最后的只能是横着放两块,否则会重复)。

      初始情况为:f[1]=1,f[2]=2。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[41];

    cin>>n;

    f[1]=1;f[2]=2; 

    for(i=3;i<=n;i++)

     f[i]=f[i-1]+f[i-2];      // 按递推关系实施递推 

    cout<<f[n]<<endl;

    return 0;

}

       (3)问题扩展。

      有一个大小是2×n的长方形方格,现用2种规格的骨牌铺满,骨牌规格分别是 2×1和2×2。输入n(n<=30),输出铺放方案的总数。

      (4)扩展问题编程思路。

      铺方格的骨牌规格多了一种,递推公式做相应变化。

      设f[i]为铺满2*n方格的方案数,则有   f[i]=f[i-1]+2*f[i-2]。

      其中,f[i-1]为铺满2*(n-1)方格的方案数(既然前面的2*(n-1)的方格已经铺满,那么最后一个只能是竖着放)。f[i-2]为铺满2*(n-2)方格的方案数(如果前面的2*(n-2)的方格已经铺满,那么最后的2*2方格或者横着放两块2*1的骨牌,或者放1块2*2的骨牌)。

      初始情况为:f[1]=1,f[2]=3。

       (5)扩展问题的源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[31];

    cin>>n;

    f[1]=1;f[2]=3; 

    for(i=3;i<=n;i++)

     f[i]=f[i-1]+2*f[i-2];      // 按递推关系实施递推 

    cout<<f[n]<<endl;

    return 0;

}

【例3】上台阶

      设有一个共有n级的台阶,某人上台阶一步可上1级,也可上2级。编写一个程序,输入台阶的级数n(n<=40),输出某人从底层上到台阶顶层的走法的种数。

      (1)编程思路。

      先考虑最简单的情况。如果只有1级台阶,那显然只有一种上法。如果有2级台阶,那就有两种上的方法了:一种是分两次上,每次上1级;另外一种就是一次上2级。

      再来讨论一般情况。设把n级台阶时的上法看成是n的函数,记为f(n)。

      当n>2时,第一次上的时候就有两种不同的选择:一是第一次只上1级,此时上法数目等于后面剩下的n-1级台阶的上法数目,即为f(n-1);另外一种选择是第一次上2级,此时上法数目等于后面剩下的n-2级台阶的上法数目,即为f(n-2)。因此n级台阶时的不同上法的总数f(n)=f(n-1)+(f-2)。

      由此推导出递推公式 f(n)=f(n-1)+(f-2)  (n>2)

      初始情况:f(1)=1; f(2)=2。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int k,n,f[41];

    cout<<"请输入台阶总数n:";

    cin>>n;

    f[1]=1;f[2]=2; 

    for(k=3;k<=n;k++)

     f[k]=f[k-1]+f[k-2];      // 按递推关系实施递推 

    cout<<"上法总数为 "<<f[n]<<endl;

    return 0;

}

      (3)扩展1。

      设一个台阶有n级,一步有m种跨法,一步跨多少级均从键盘输入(输入的级数从小到大)。求从底层上到台阶顶层的走法的种数。

      (4)扩展1的编程思路。

      例如,设有10级台阶,输入的m依次为2、4、7。递推式可写成

       f(n)=f(n-2)+f(n-4)+f(n-7)  (n>=8)。但由于事先不知输入的是2、4、7,所以递推初始情况f(1)~f(7)无法直接赋初值,也就无法用一个这个单纯的递推式直接递推。必须先采用某种方法根据输入的m个一步上的台阶数将初始条件求取出来。

      实际上,可以将上台阶的递推分成多级递推,这样初始条件可在分级递推中求取。

当第1次输入2时,f(1)=0,f(2)=1。(初始条件)

当第2次输入4时,第1级递推:

f(3)=f(3-2)=0,  f(4)=f(4-2)+1=2。(因为4本身即为一个一步到位的上法)

当第3次输入7时,第2级递推:

f(5)=f(5-2)+f(5-4)=0,f(6)=f(6-2)+f(6-4)=3,f(7)=f(7-2)+f(7-4)+1=1。

为求第10级台阶上法,采用三级递推:

f(8)=f(8-2)+f(8-4)+f(8-7)=5,f(9)=f(9-2)+f(9-4)+f(9-7)=2,

f(10)=f(10-2)+f(10-4)+f(10-7)=8。

下面探讨一般情况。

      设上n级台阶的不同上法为f(n),从键盘输入一步跨多少级的m个整数分别为x(1)、x(2)、…、x(m),输入时约定1<=x(1) <x(2) <…<x(m)。

当1<=n<x(1) 时,f(n)=0;      f(x(1))=1。(初始条件)

当x(1)<n<x(2) 时,第1级递推:f(n)=f(n−x(1));

  f(x(2))=f(n-x(1))+1 (存在x2这个一步到位的上法)。

当x(2)<n<x(3) 时,第2级递推:f(n)=f(n−x(1))+f(n−x(2));

  f(x(3))=f(n-x(1))+ f(n−x(2)) +1 (存在x3这个一步到位的上法)。

……

当x(m-1)<n<x(m),有第m-1级递推:f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m-1));

f(x(m))=f(n−x(1))+f(n−x(2))+…+f(n-x(m-1))+1。

当x(m)<n时,第m级递推: f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m))

为了便于统一处理,不妨附设一个x(m+1),使得x(m+1)=max(x(m),n)+1。

这样第m级递推统一描述为:

       当x(m)<n<x(m+1) 时,第m级递推: f(n)=f(n−x(1))+f(n−x(2))+…+f(n−x(m))

          f(x(m+1))= f(n−x(1))+f(n−x(2))+…+f(n−x(m))+1。

      (5)扩展1的源程序。

#include <iostream>

using namespace std;

int main()

{

       int i,j,k,m,n,t,x[10],f[51];

    cout<<"请输入总台阶数:";

    cin>>n;

    cout<<"一次有几种跳法:";

    cin>>m;

    cout<<"请从小到大输入一步跳几级。"<<endl;

    for(i=1; i<=m; i++)    

        cin>>x[i];

    for(i=1;i<x[1];i++)   f[i]=0;  

    f[x[1]]=1;

    x[m+1]=(x[m]>n?x[m]:n)+1;

       for(k=1;k<=m;k++)

        for(t=x[k]+1;t<=x[k+1];t++)

              {

                     f[t]=0;

            for(j=1;j<=k;j++)             // 按公式累加实现分级递推 

               f[t]=f[t]+f[t-x[j]];

            if(t==x[k+1])             

               f[t]=f[t]+1;

              }

       cout<<"共有不同的跳法种数为 "<<f[n]<<endl;

       return 0;

}

      (6)扩展2。

      例3中的递推式实际上就是斐波那契数列的递推式。这个数列的增长很快,46项以后就会超过整型数据的表数范围。现设台阶有1000级,按一步可上1级,也可上2级的上法,不同的上法种数是一个209位的整数,如何正确求得这个种数。

    (7)扩展2的编程思路。

      由于要求的数据超过了整数表示的范围,需要进行高精度计算。

      如何表示和存放大数呢?一个简单的方法就是:用数组存放和表示大数。一个数组元素,存放大数中的一位。

      我们日常书写一个大整数,左侧为其高位,右侧为其低位,在计算中往往会因进位(carry)或借位(borrow)导致高位增长或减少,因此可以定义一个整型数组(int bignum[maxlen])从低位向高位实现大整数的存储,数组的每个元素存储大整数中的一位。

      显然,在C++中,int类型(4个字节/32位计算机)数组元素存储十进制的一位数字非常浪费空间,并且运算量也非常大,因此可将存储优化为万进制,即数组的每个元素存储大整数数字的四位。(为什么选择万进制,而不选择更大的进制呢?这是因为万进制中的最大值9999相乘时得到的值是99980001,不会超过4个字节的存储范围,而十万进制中的最大值99999相乘时得到的值是9999800001,超过4个字节的存储范围而溢出,从而导致程序计算错误。)

      在编写程序代码过程中作如下定义:

const int base=10000;

const int maxlen=50+1;

int bignum[maxlen];

      说明:base表示进制为万进制,maxlen表示大整数的长度,1个元素能存储4个十进制位,50个元素就存储200个十进制位,而加1表示下标为0的元素另有它用,程序用作存储当前大整数的位数。

      下面讨论大整数的加法运算的实现。

      可以采用小学中曾学习的竖式加法。两个大整数98240567043826400046和3079472005483080794进行加法运算,如图2所示。

 

图2  加法的计算过程

      从图2中可以得知,做加法运算是从低位向高位进行,如果有进位,下一位进行相加时要加上进位,如果最高位已计算完还有进位,就要增加存储结果的位数,保存起进位来。关于进位的处理,一般定义单独变量carry进行存储。

      (8)扩展2的源程序。

#include <iostream>

using namespace std;

const int base=10000;

const int maxlen=60+1;

void addition(int *bignum1, int *bignum2, int *bignum_ans);

void printbignum(int *bignum);

int main()

{

    int k,n,f[1001][maxlen];

    cout<<"请输入台阶总数n:";

    cin>>n;

    f[1][0]=1;  f[1][1]=1;

       f[2][0]=1;  f[2][1]=2;

    for(k=3;k<=n;k++)

      addition(f[k-1],f[k-2],f[k]);      // 按递推关系实施递推 

    cout<<"上法总数为 ";

       printbignum(f[n]);

    return 0;

}

void addition(int *bignum1, int *bignum2, int *bignum_ans)

{

       int carry=0;

    memset(bignum_ans,0,sizeof(int)*maxlen);

       bignum_ans[0]=bignum1[0]>bignum2[0]?bignum1[0]:bignum2[0];

       for(int pos=1; pos<=bignum_ans[0]; pos++){

              carry+=bignum1[pos]+bignum2[pos];

              bignum_ans[pos]=carry%base;

              carry/=base;

       }

       if(carry)

              bignum_ans[++bignum_ans[0]]=carry;

}

void printbignum(int *bignum)

{

       int *p=*bignum+bignum;

       cout<<*p--;

       cout.fill('0');        // 定义填充字符'0'

       while(p>bignum){ cout.width(4); cout<<*p--; }

       cout<<endl;

}

      如果要求2000级台阶的不同上法种数(是一个418位的整数),上面的程序如何修改?如果求5000级甚至10000级的台阶呢?请读者自己动手做一做。在修改过程中,定义的二维数组造成栈溢出怎么办?

      (9)扩展3——第39级台阶。

      小明刚刚看完电影《第39级台阶》,离开电影院的时候,他数了数礼堂前的台阶数,恰好是39级! 站在台阶前,他突然又想着一个问题:

       如果我每一步只能迈上1个或2个台阶。先迈左脚,然后左右交替,最后一步是迈右脚,也就是说一共要走偶数步。那么,上完39级台阶,有多少种不同的上法呢?

       (10)扩展3的编程思路。

       由于在上台阶的过程中需要考虑不同的迈脚情况,因此需要定义二维数组c[40][2],数组元素c[i][0]表示上到第i级台阶时最后一步是左脚的方法数,c[i][1] 表示上到第i级台阶时最后一步是右脚的方法数。

由于每一步能上一级或两级台阶,因此可得递推式:

c[i][0] = c[i-1][1]+c[i-2][1] (因为最后一步为左脚,倒数第二步肯定为右脚)

c[i][1] = c[i-1][0]+c[i-2][0] (因为最后一步为右脚,倒数第二步肯定为左脚)

初始情况为:

c[1][0]=1,第一步左脚直接迈一级台阶。

c[1][1]=0,因为先迈左脚,右脚不可能落在第1级台阶。

c[2][0]=1,第一步左脚直接迈两级台阶。

c[2][1]=1,左右脚各迈一级台阶,右脚正好落在第2级台阶。

(11)扩展3的源程序。

#include<iostream>

using namespace std;

int main()

{

    int i;

    int c[40][2];

    c[1][1] = 0;   c[1][0]=1;

    c[2][1] = 1;   c[2][0]=1;

    for(i = 3; i <= 39; ++i)

       {

        c[i][0] = c[i - 1][1] + c[i - 2][1];

        c[i][1] = c[i - 1][0] + c[i - 2][0];

    }

    cout<<c[39][1]<<endl;

    return 0;

}

     

       例2和例3采用的都是顺推,下面看一个采用倒推求解的实例。

      【例4】猴子吃桃

      有一个猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半后又多吃一个。到第10天早上想再吃时,见只剩下一个桃子了。

求第一天共摘了多少个桃子?

(1)编程思路。

设x[n]表示第n天吃桃子前的桃子数,则有x[10]=1。

由于, x[i]=x[i-1]-x[i-1]/2-1,

因此,递推关系式为: x[i-1]=2*(x[i]+1)。倒推求得x[1]就是猴子所摘的桃子数。

(2)源程序。

#include <iostream>

using namespace std;

int  main()

{

    int a[11],i;

       a[10]=1;

       for (i=9;i>=1;i--)

              a[i]=2*(a[i+1]+1);

       cout<<a[1]<<endl;

    return  0;

}

      (3)问题扩展。

      有一猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了m个。第二天早上又将剩下的桃子吃掉一半,又多吃了m个。以后每天早上都吃了前一天剩下的一半后又多吃m个。到第n天早上想再吃时,见只剩下d个桃子了。

      求第一天共摘了多少个桃子(m,n,d均由键盘输入,且1<=n,m,d<=20)?

      (4)扩展问题的源程序。

#include <iostream>

using namespace std;

int  main()

{

    int i,m,n,d;

    cin>>n>>m>>d;

       int *a;

       a=new int [n+1];

       a[n]=d;

       for (i=n-1;i>=1;i--)

              a[i]=2*(a[i+1]+m);

       cout<<a[1]<<endl;

    return  0;

}

 【例5】过河卒

      如图3,在棋盘的A点有一个过河卒,需要走到目标B点。卒行走规则:可以向下、或者向右。同时在棋盘上的任一点有一个对方的马(如图4-4的C点),该马所在的点和所有跳跃一步可达的点称为对方马的控制点。例如,图4-4中C点上的马可以控制9个点(图中的P1,P2,…,P8 和C)。卒不能通过对方马的控制点。

       棋盘用坐标表示,A点(0,0)、B点(n,m)(n,m为不超过50的整数,并由键盘输入),同样马的位置坐标通过键盘输入,并约定C<>A,同时C<>B。

       编写程序,计算出卒从A点能够到达B点的路径的条数。

       例如,输入6  6  3  2,输出为17。

 

图3  棋盘上的过河卒和对方的控制马

(1)编程思路。

      在棋盘的A点(0,0)的过河卒要到达棋盘上的任意一点,只能从左边和上边两个方向过来。因此,要到达某一点的路径数,等于和它相邻的左、上两个点的路径数和:

        F[i][j] = F[i-1][j] + F[i][j-1]。

      可以使用逐列(或逐行)递推的方法来求出从起始顶点到终点的路径数目,即使有障碍(将马的控制点称为障碍),这一递推方法也完全适用,只要将到达该点的路径数目设置为0即可,用F[i][j]表示到达点(i,j)的路径数目,g[i][j]表示点(i,j)有无障碍,递推方程如下:

F[0][0] = 1     初始点直接可达。

F[i][0] = F[i-1][0]   (i > 0,g[i][0] =0)     // 左边界

F[0][j] = F[0][j-1]   (j > 0,g[0][j] = 0)    // 上边界

F[i][j] = F[i-1][j] + F[i][j-1]   (i > 0,j > 0,g[x, y] = 0) // 递推式

(2)源程序。

#include <iostream>

using namespace std;

int main() 

    int i,j,x,y,n,m,forbidden[51][51]; 

    int ans[51][51]; 

    int dx[8]={-2,-1,1,2,2,1,-1,-2};

       int dy[8]={1,2,2,1,-1,-2,-2,-1};

    cin>>n>>m>>x>>y; 

    for (i=0;i<=n;i++)

              for (j=0;j<=m;j++)

        {

                     forbidden[i][j]=0;

            ans[i][j]=0;

              }

    ans[0][0]=1; 

    forbidden[x][y]=1;

       for (i=0;i<8; i++)

        if (x+dx[i]>=0 && x+dx[i]<=n && y+dy[i]>=0 && y+dy[i]<=m)

                 forbidden[x+dx[i]][y+dy[i]]=1;

       for (i=1; i<=n; i++) 

        if (forbidden[i][0]==0) 

            ans[i][0]=1; 

        else break; 

    for (i=1; i<=m; i++) 

        if (forbidden[0][i]==0) 

            ans[0][i]=1; 

        else break; 

    for (i=1; i<=n; i++) 

        for (j=1; j<=m; j++) 

            if (forbidden[i][j]==0) 

                ans[i][j]=ans[i-1][j]+ans[i][j-1]; 

    cout<<ans[n][m]<<endl;

    return 0; 

【例6】学生队列。

There are many students in PHT School. One day, the headmaster whose name is PigHeader wanted all students stand in a line. He prescribed that girl can not be in single. In other words, either no girl in the queue or more than one girl stands side by side.

The case n=4 (n is the number of children) is like FFFF, FFFM, MFFF, FFMM, MFFM, MMFF, MMMM Here F stands for a girl and M stands for a boy. The total number of queue satisfied the headmaster’s needs is 7.

Can you make a program to find the total number of queue with n children?

      (1)编程思路。

      设满足要求的n个学生的队列数为F(n)表示。

      简单枚举n较小的情况为:F(0)=1 (没有人也是合法的,这个可以特殊处理,就像0的阶乘定义为1一样); 

        F(1)=1(一个男生M);F(2)=2;(两个男生MM或两个女生FF);F(3)=4(MMM、MFF、FFM或FFF)。

      当人数n大于3时,n个学生排队可以看成是由n-1个学生排好队后再加一个学生。按最后加的学生的性别分两种情况:

      1)当加的最后一个学生是男孩M时候,前面n-1个随便排出来,只要符合要求就可以,即方案数为F(n-1)。

      2)当加的最后一个学生是女孩F时候,第n-1个学生肯定是女孩F,这时候又有两种情况:

      ① 前面n-2个学生按满足要求的方法排好队,后面加两个女生一定也满足要求,此时方案数为F(n-2)。

      ② 前面n-2个人不是满足要求的队列,加上两个女生也有可能是合法的。当第n-2个学生是女孩而第n-3个学生是男孩时,后面加上两个女孩可能合法。此时前n-4个学生的队列必须满足要求,即方案数为F(n-4)。

      综上所述:总数F(n)= F(n-1)+ F(n-2)+ F(n-4)。

      (2)源程序。

#include <iostream>

using namespace std;

int main()

{

    int i,n,f[31];

    f[0]=1; f[1]=1; f[2]=2; f[3]=4; 

    for(i=4;i<=30;i++)

       f[i]=f[i-1]+f[i-2]+f[i-4];      // 按递推关系实施递推 

    while (cin>>n && n!=-1)

       {

              cout<<f[n]<<endl;

       }

    return 0;

}

【例7】栈

      栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。

      栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈)。

      栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。晶晶宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而她自己无法给出答案,所以需要你的帮忙。

      晶晶考虑的是这样一个问题:一个操作数序列,从1,2,一直到n(图4-5所示为1到3的情况),栈A的深度大于n。

现在可以进行两种操作,

1)将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的push操作)。

2)将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的pop操作)。

      使用这两种操作,由一个操作数序列就可以得到一系列的输出序列。设栈的原始状态如图4所示,由123生成序列231的过程如图5所示。

 

图4  栈的原始状态

 

图5  采用栈生成序列231的过程

      请编写程序,对输入的n,计算并输出由操作数序列1,2,…,n经过栈操作可能得到的输出序列的总数。

      (1)编程思路。

       先模拟入栈、出栈操作,看看能否找出规律,设f(n)表示n个数通过栈操作后的排列总数,当n很小时,很容易模拟出f(1)=1,f(2)=2,f(3)=5。通过观察,看不出它们之间的递推关系,再分析n=4的情况,假设入栈前的排列为“4123”,按第一个数“4”在出栈后的位置进行分情况讨论:

      1)若“4”最先输出,刚好与n=3相同,总数为f(3)。

      2)若“4”第二个输出,则在“4”的前只能是“1”,“23”在“4”的后面,这时可以分别看作是n=1和n=2时的两种情况,排列数分别为f(1)和f(2),所以此时的总数为f(1)*f(2)。

      3)若“4”第三个输出,则“4”的前面二个数为“12”,后面一个数为“3”,组成的排列总数为f(2)*f(1)。

      4)若“4”第四个输出,与情况(1)相同,总数为f(3)。

     所以有:f(4)=f(3)+f(1)*f(2)+f(2)*f(1)+f(3)。

      若设0个数通过栈后的排列总数为:f(0)=1;

      上式可变为:f(4)=f(0)*f(3)+f(1)*f(2)+f(2)*f(1)+f(3)*f(0)。

再进一步推导,不难推出递推式:

f(n)=f(0)*f(n-1)+f(1)*f(n-2)+…+f(n-1)*f(0);

即f(n)=      (n>=1)

初始值:f(0)=1;

有了以上递推式,就很容易用递推法写出程序。

(2)源程序。

#include <iostream>

using namespace std;

int  main()

{

    int a[19]={0},n,i,j;

       cin>>n;

       a[0]=1;

       for (i=1;i<=n;i++)

              for (j=0;j<=i-1;j++)

                     a[i]=a[i]+a[j]*a[i-j-1];

       cout<<a[n]<<endl;

    return  0;

}

      (3)用公式直接求解。

      实际上,一个栈的入栈序列为1,2,3,…,n,其不同的出栈序列的种数是一个卡特兰数。

      卡特兰数是组合数学中一个常出现在各种计数问题中出现的数列。该数列如下:

C0=1,C1=1,C2=2,C3=5,C4=14,C5=42,C6=132,C7=429,C8=1430,

C9=4862,C10=16796,C11=58786,C12=208012,……。

卡塔兰数的一般项公式为:

 

        Cn的另一个表达形式为:

 

 

下面简要描述一下这个公式的推导情况。

      对于1~n中的每一个数来说,必须入栈一次、出栈一次。设入栈为状态“1”,出栈为状态“0”。n个数的所有状态对应n个1和n个0组成的2n位二进制数。由于等待入栈的操作数按照1~n的顺序排列、入栈的操作数b大于等于出栈的操作数a(a≤b),因此输出序列的总数目等于由左至右扫描由n个1和n个0组成的2n位二进制数,1的累计数不小于0的累计数的方案种数。

      在2n位二进制数中填入n个1的方案数为c(2n,n),不填1的其余n位自动填0。从中减去不符合要求(由左至右扫描,0的累计数大于1的累计数)的方案数即为所求。

      不符合要求的数的特征是由左至右扫描时,必然在某一奇数位2m+1位上首先出现m+1个0的累计数和m个1的累计数,此后的2(n-m)-1位上有n-m个 1和n-m-1个0。如果把后面这2(n-m)-1位上的0和1互换,使之成为n-m个0和n-m-1个1,结果得1个由n+1个0和n-1个1组成的2n位数,即一个不合要求的数对应于一个由n+1个0和n-1个1组成的排列。反过来,任何一个由n+1个0和n-1个1组成的2n位二进制数,由于0的个数多2个,2n为偶数,故必在某一个奇数位上出现0的累计数超过1的累计数。同样在后面部分0和1互换,使之成为由n个0和n个1组成的2n位数,即n+1个0和n-1个1组成的2n位数必对应一个不符合要求的数。因而不合要求的2n位数与n+1个0,n-1个1组成的排列一一对应。 即不符合要求的方案数为c(2n,n+1)。

       由此得出,输出序列的总数目= c(2n,n)-c(2n,n+1)=c(2n,n)/(n+1)。

      (4)采用公式直接求解的源程序。

#include <iostream>

using namespace std;

int combin(int n,int m)   // 求组合数C(n,m)

{

       int p=1,i,t;

    t=n;

       for (i=1;i<=m;i++)

       {

        p=p*t/i;

              t--;

       }

       return p;

}

int main() 

    int n; 

    cin>>n; 

    cout<<combin(2*n,n)/(n+1)<<endl;

    return 0; 

 

【例8】整数划分问题

      正整数s(简称为和数)的划分(又称拆分)是把s分成为若干个正整数(简称为零数或部分)之和,划分式中允许零数重复,且不记零数的次序。

      试求s=12共有多少个不同的划分式?展示出所有这些划分式。

(1)编程思路。

整数划分问题可采用多种方法解决,下面我们采用递推的法来完成。

为了建立递推关系,先对和数k较小时的划分式作观察归纳:

k=2:1+1;2    2种划分式

k=3:1+1+1;1+2;3      3种划分式

k=4:1+1+1+1;1+1+2;1+3;2+2;4     5种划分式

k=5:1+1+1+1+1;1+1+1+2;1+1+3;1+2+2;1+4;2+3;5    7种划分式

      由以上各划分看到,除和数本身k=k这一特殊划分式外,其它每一个划分式至少为两项之和。约定在所有划分式中零数作非降序排列,探索和数k的划分式与和数k−1的划分式存在以下递推关系:

      1)在所有和数k−1的划分式前加零数1都是和数k的划分式。

      2)和数k−1的划分式的前两个零数作比较,如果第1个零数x1小于第2个零数x2,则把第1个零数加1后成为和数k的划分式。

定义一个三维数组a,a[k][j][i]表示为和数k的第j个划分式的第i个数。

递推的初始条件为:a[2][1][1]=1; a[2][1][2]=1; a[2][2][1]=2。

根据递推关系,实施递推:

1)实施在k−1所有划分式前加1操作。

a[k][j][1]=1;

for (t=2;t<=k;t++)         

        a[k][j][t]=a[k−1][j][t−1];          //  k−1的第t−1项变为k的第t项

2)若k−1划分式第1项小于第2项,第1项加1,变为k的第i个划分式

       if(a[k-1][j][1]<a[k-1][j][2])  // 若k-1划分式第1项小于第2项 

         { i++;                     // 第1项加1为k的第i个划分式的第1项 

          a[k][i][1]=a[k-1][j][1]+1;    

          for(t=2;t<=k-1;t++)

             a[k][i][t]=a[k-1][j][t];

          }

(2)源程序及运行结果。

#include <iostream>

#include <iomanip>

using namespace std;

int main()

{

       int s,i,j,k,t,cnt;

    static int a[21][800][21]={0};

    cout<<"input s(s<=20):";

       cin>>s;

    a[2][1][1]=1; a[2][1][2]=1; a[2][2][1]=2;

    cnt=2;

    for(k=3;k<=s;k++)

    {

              for(j=1;j<=cnt;j++)

        {

                     a[k][j][1]=1;          

            for(t=2;t<=k;t++)    // 实施在k-1所有划分式前加1操作 

               a[k][j][t]=a[k-1][j][t-1];    

        }

        for(i=cnt,j=1;j<=cnt;j++)

           if(a[k-1][j][1]<a[k-1][j][2])  // 若k-1划分式第1项小于第2项 

                 {

                        i++;              // 第1项加1为k的第i个划分式的第1项 

               a[k][i][1]=a[k-1][j][1]+1;    

               for(t=2;t<=k-1;t++)

                  a[k][i][t]=a[k-1][j][t];

           }

        i++;   a[k][i][1]=k;    // k的最后一个划分式为:k=k 

        cnt=i;

       }

    for(j=1;j<=cnt;j++)        // 输出s的所有划分式 

    {

              cout<<setw(4)<<j<<": "<<s<<"="<<a[s][j][1];

        i=2;

        while (a[s][j][i]>0)

        {

                     cout<<"+"<<a[s][j][i];

                     i++;

              }

        cout<<endl;

       }

    return 0;

}

      (3)整数划分的优化。

      上面递推算法的时间复杂度与空间复杂度为O(n2u),其中u为n划分式的个数。由于u随n增加非常快,难以估算其数量级,因此算法的时间复杂度与空间复杂度是很高的。

      下面我们对整数划分进行集智。

      分析上面使用三维数组a[k][j][i]完成的递推过程。当由k−1的划分式推出k的划分式时,k−1以前的数组单元已完全闲置。因此,可以考虑把三维数组a[k][j][i]改进为二维数组a[j][i]。进行和数为k的递推前,数组元素a[j][i]表示和数是k−1的已有划分式。根据递推关系推出和数为k的划分式:

       1)把a[j][i]依次存储到a[j][i+1],加上第一项a[j][1]=1;这样完成在k−1的所有划分式前加1的操作,转化为k的划分式。

for(t=i;t>=1;t−−)

       a[j][t+1]=a[j][t];

a[j][1]=1;

       2)对已转化的cnt个划分式逐个检验,若其第2个数小于第3个数(相当于k−1时的第1个数小于第2个数),则把第2个数加1,去除第一个数后,作为k时增加的一个划分式,为第t个(t从cnt开始,每增加一个划分式,t增1)划分式。

for(t=u,j=1;j<=u;j++)

  if(a(j,2)<a(j,3))     // 若k−1划分式第1项小于第2项 

    {t++;

     a(t,1)=a(j,2)+1;  // 第1项加1 作为k的第t个划分式的第1项

     i=3;

     while(a(j,i)>0)

        {a(t,i−1)=a(j,i);i++;}

     }   

      (4)优化后的源程序。

#include <iostream>

#include <iomanip>

using namespace std;

int main()

{

       int s,i,j,k,t,cnt;

    static int a[1600][25]={0};

    cout<<"input s(s<=24):";

    cin>>s;

    a[1][1]=1;a[1][2]=1;a[2][1]=2;

       cnt=2;

    for(k=3;k<=s;k++)

    {

              for(j=1;j<=cnt;j++)

        {

                     i=k-1;          

            for(t=i;t>=1;t--)         // 实施在k-1所有划分式前加1操作 

              a[j][t+1]=a[j][t];

            a[j][1]=1;

              }

        for(t=cnt,j=1;j<=cnt;j++)

          if(a[j][2]<a[j][3])   // 若k-1划分式第1项小于第2项 

                {

                       t++;

              a[t][1]=a[j][2]+1;   // 第1项加1 

              i=3;

              while(a[j][i]>0)

              {

                              a[t][i-1]=a[j][i];

                              i++;

                       }

                }

        t++;  a[t][1]=k;             // 最后一个划分式为:k=k 

        cnt=t;

       }

    for(j=1;j<=cnt;j++)        // 输出s的所有划分式 

    {

              cout<<setw(4)<<j<<": "<<s<<"="<<a[j][1];

        i=2;

        while (a[j][i]>0)

        {

                     cout<<"+"<<a[j][i];

                     i++;

              }

        cout<<endl;

       }

    return 0;

}

      改进的递推程序把原有的三维数组优化为二维数组,降低了算法的空间复杂度,拓展了算法的求解范围。

      但是,由于划分式的数量cnt随和数s增加得相当迅速。例如,和数为24时有1575种划分。因此尽管改进为二维数组,求解的和数s也不可能太大。

posted on 2019-06-14 11:59  aTeacher  阅读(1674)  评论(0编辑  收藏  举报