递推(二):递推法的应用
下面通过一些典型实例及其扩展来讨论递推法的应用。
【例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也不可能太大。