遞歸(二):正整數的拆分


【例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;

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM