【例1】求正整數的拆分數。
將正整數s表示成一系列正整數之和,s=n1+n2+…+nk,其中n1>=n2>=…>=nk, k>=1。正整數s的不同拆分個數稱為s的拆分數。例如,正整數6有11種不同的拆分,分別是:
6; 5+1; 4+2; 4+1+1; 3+3; 3+2+1; 3+1+1+1;
2+2+2; 2+2+1+1; 2+1+1+1+1; 1+1+1+1+1+1。
(1)編程思想。
設m、n均為正整數,m可表示為一些不超過n的正整數之和,f(m,n)為這種表示方式的數目。下面先確定遞歸關系。
如果 n>m,則拆分式中n、n-1、…、m+2、m+1這n-m 個數必定不會出現,去掉它們對拆分式的表示數目不產生影響;即 f(m,n) = f(m,m)。
如果 n=m,則 f(n,m)=1+f(n,n−1)。 其中,“1”表示n的拆分式中只包含n本身,即n=n,只有一種拆分表示;f(n,n−1)表示n的所有其他拆分,即拆分式中最大正整數不超過n−1的拆分數目。
如果n<m,則 f(n,m)=f(n,m−1)+f(n−m,m)。其中,f(n,m−1)表示拆分式中不包含m的拆分式數目;f(n−m,m)表示拆分式中至少包含一個m的拆分數目,因為如果確定了一個拆分式中包含正整數m,則剩下的部分就是對n−m進行不超過m的拆分。
確定遞歸的終止條件。第一個終止條件:f(n,1)=1,表示當拆分式中最大的正整數是1時,該整數n只有一種拆分,即n個1相加。第二個終止條件:f(1,m)=1,表示整數n=1只有一個拆分,不管上限m是多大。
(2)源程序。
#include <iostream>
using namespace std;
int f(int m,int n)
{
if(m==1 || n==1)
return 1 ;
if(m<n)
return f(m,m);
if (m==n)
return 1+ f(m,n-1);
return f(m,n-1)+f(m-n, n );
}
int main()
{
int n, m,k;
while (cin >>m>>n && n!=0)
{
k=f(m,n);
cout<<k<<endl;
}
return 0;
}
【例2】正整數的拆分式。
正整數s(簡稱為和數)的拆分是把s分成為某些指定正整數(簡稱為零數)之和,拆分式中不允許零數重復,且不記零數的次序。
把指定正整數s拆分為1~m(m<=s)之和,共有多少種不同的拆分方式?輸出所有這些拆分式。
例如,若s=6,m=6,則例1中的11個拆分式只有4個符合本題的要求。即 6; 5+1; 4+2; 3+2+1。
(1)編程思路。
由於正整數的拆分與拆分式中各零數的排列順序無關,因此,可以將正整數s拆分式表示成一系列從大到小排列的正整數之和,即 s=n1+n2+…+nk,其中m>=n1>n2>…>nk>=1, k>=1。
拆分式中的k個零數都在1~m之間,因此我們需要先解決如何從1~m這m個數中取k(k<m)個數的所有組合。
設 comb(int a[],int m,int k)為從1~m這m個數中取k個數的所有組合結果。組合的結果保存在數組元素a[1]~a[k]中,數組元素a[0]表示組合結果中元素的個數,顯然,a[0]=k。
為求解comb(int a[],int m,int k),可以先將k個數字組合的第一個數字i放在a[k]中,第一個數字i可以是m,m-1,…,k。注意:第一個數字i不能取k-1,因為后面的數字都會取比第一個數字小的數字,因此最多只能取出1~k-1共k-1個不同的數,達不到取k個數的目的。
在將確定組合的第一個數字放入數組后,有兩種選擇:還未確定組合的其余元素時(k>1,即還需取k-1個元素),繼續遞歸comb(a,i-1,k-1)確定組合的其余元素,即在1~i-1中取k-1個數;已確定組合的全部元素時(k==1),輸出這個組合。
實現這一取數組合的遞歸函數可設計為:
void comb(int a[],int m,int k)
{
int i,j;
for (i=m;i>=k;i--)
{
a[k]=i;
if(k>1)
comb(a,i-1,k-1);
else
{
for (j=a[0];j>=1;j--)
cout<<a[j]<<" ";
cout<<endl;
}
}
}
解決了組合取數后,對所選取的k個數,求其和t並與和數s進行比較:若t=s,即找到一個拆分式,輸出拆分式,並設置變量cnt統計拆分式的個數。可將上面的遞歸函數改寫為:
void comb(int a[],int m,int k,int s) // 加了一個參數s用於傳遞和數
{
int i,j,t;
for (i=m;i>=k;i--)
{
a[k]=i;
if (k>1)
comb(a,i-1,k-1,s);
else
{
for(t=0,j=a[0];j>0;j--) // 先計算所取數的和值t
t=t+a[j];
if(t==s) // 取數組合的和值等於所求和數s,才輸出結果
{
cnt++;
cout<<s<<"=";
for(j=a[0];j>1;j--)
cout<<a[j]<<"+";
cout<<a[1]<<endl;
}
}
}
}
由於題目要求把指定正整數s拆分為1~m(m<=s)之和,因此拆分式中的數字個數可以為1個,也可以為2個,最多為m個,因此主函數中采用一個循環簡單調用遞歸函數即可:
for (i=1;i<=m;i++)
{
a[0]=i;
comb(a,m,i,s);
}
(2)源程序及運行結果示例。
#include <iostream>
using namespace std;
int cnt=0;
void comb(int a[],int m,int k,int s)
{
int i,j,t;
for(i=m;i>=k;i--)
{
a[k]=i;
if(k>1)
comb(a,i-1,k-1,s);
else
{
for(t=0,j=a[0];j>0;j--)
t=t+a[j];
if(t==s)
{
cnt++; cout<<s<<"=";
for(j=a[0];j>1;j--)
cout<<a[j]<<"+";
cout<<a[1]<<endl;
}
}
}
}
int main()
{
int a[100],s,m,i;
while (cin>>s>>m && s!=0)
{
cnt=0;
for (i=1;i<=m;i++)
{
a[0]=i;
comb(a,m,i,s);
}
cout<<cnt<<endl;
}
return 0;
}
程序運行示例如下:
6 6
6=6
6=5+1
6=4+2
6=3+2+1
4
20 10
20=10+9+1
20=10+8+2
20=10+7+3
20=10+6+4
20=9+8+3
20=9+7+4
20=9+6+5
20=8+7+5
20=10+7+2+1
20=10+6+3+1
20=10+5+4+1
20=10+5+3+2
20=9+8+2+1
20=9+7+3+1
20=9+6+4+1
20=9+6+3+2
20=9+5+4+2
20=8+7+4+1
20=8+7+3+2
20=8+6+5+1
20=8+6+4+2
20=8+5+4+3
20=7+6+5+2
20=7+6+4+3
20=10+4+3+2+1
20=9+5+3+2+1
20=8+6+3+2+1
20=8+5+4+2+1
20=7+6+4+2+1
20=7+5+4+3+1
20=6+5+4+3+2
31
0 0
Press any key to continue
(3)主調函數優化。
前面給出的主函數中采用一個循環簡單調用遞歸函數。
for (i=1;i<=m;i++)
{
a[0]=i;
comb(a,m,i,s);
}
循環變量i代表拆分式中零數的個數,其范圍從1到m。這樣會進行大量無效的搜索。
以程序運行示例中的s=20,m=10進行分析說明。
要把指定的正整數 20 拆分為1~10之和,主函數中零數個數從1取到10進行搜索。實際上,最大的兩個數之和10+9=19小於20,最小的6個數之和 1+2+3+4+5+6=21大於20。也就是說,拆分式中零數的個數只可能是3、4、5這3種情況。因此,前面循環中無需對i的取值1,2,6,7,8,9,10這些情況進行遞歸處理,而遞歸的核心是從1~m這m個數中取i個數的進行組合。這樣,相當於少處理了C(10,1)+C(10,2)+C(10,6)+C(10,7)+C(10,8)+C(10,9)+C(10,10)=10+45+210+120+45+10+1=441種情況。
主函數的優化思路是:對於給定的和數s與最大零數m,首先計算拆分式中零數的最少個數min與零數的最多個數max,顯然,拆分式中零數的個數I取在區間[min,max]中。
按這個思路將主函數修改如下:
int main()
{
int a[100],s,m,i,t,min,max;
while (cin>>s>>m && s!=0)
{
cnt=0;
for (t=0,i=1;i<=m;i++)
{
t=t+i;
if (t>s) { max=i-1; break;}
else if (t==s) { max=i; break; }
}
if(i>m) // 輸入的最大零數太小
{
cout<<"輸入的最大零數太小!1~"<<m<<"的和為"<<t<<",小於"<<s<<endl;
continue;
}
for (t=0,i=1;i<=m;i++)
{
t=t+(m-i+1);
if (t>s) { min=i; break;}
else if (t==s) { min=i; break; }
}
for (i=min;i<=max;i++)
{
a[0]=i;
comb(a,m,i,s);
}
cout<<cnt<<endl;
}
return 0;
}
【例3】拆分為指定整數之和。
把指定正整數s拆分為m個指定整數b1,b2…,bm之和,共有多少種不同的拆分法?輸出所有這些拆分式。
例如,輸入 零數的個數m=6
依次由小到大輸入指定零數分別為:1,2,3,5,6,9
輸入和數 s=15。
程序應輸出拆分式為:
15= 9+ 6
15= 9+ 5+ 1
15= 9+ 3+ 2+ 1
15= 6+ 5+ 3+ 1
(1)編程思路。
定義一個數組b保存指定的零數。可以在例2程序的基礎上,b數組的下標用a數組的值替代。求和過程中把例2程序中對a數組元素的求和 t=t+a[j] 改為對b數組元素求和 t=t+b[a[j]],其中a[j]為b數組的下標。
(2)源程序。
#include <iostream>
using namespace std;
int cnt=0;
void comb(int a[],int m,int k,int s,int b[])
{
int i,j,t;
for(i=m;i>=k;i--)
{
a[k]=i;
if(k>1)
comb(a,i-1,k-1,s,b);
else
{
for(t=0,j=a[0];j>0;j--)
t=t+b[a[j]];
if(t==s)
{
cnt++; cout<<s<<"=";
for(j=a[0];j>1;j--)
cout<<b[a[j]]<<"+";
cout<<b[a[1]]<<endl;
}
}
}
}
int main()
{
int a[100],b[100],s,m,i,t,min,max;
cout<<"輸入零數的個數:";
while (cin>>m && m!=0)
{
cnt=0;
cout<<"依次由小到大輸入指定的整數:";
for(i=1;i<=m;i++)
cin>>b[i];
cout<<"輸入和數為:";
cin>>s;
for (t=0,i=1;i<=m;i++)
{
t=t+b[i];
if (t>s) { max=i-1; break;}
else if (t==s) { max=i; break; }
}
if(i>m)
{ cout<<"輸入的指定整數的和為"<<t<<",小於"<<s<<endl;
continue;
}
for (t=0,i=1;i<=m;i++)
{
t=t+b[m-i+1];
if (t>s) { min=i; break;}
else if (t==s) { min=i; break; }
}
for (i=min;i<=max;i++)
{
a[0]=i;
comb(a,m,i,s,b);
}
cout<<cnt<<endl;
}
return 0;
}