複製鏈接
請複製以下鏈接發送給好友

整數分拆

鎖定
整數分拆理論,主要是研究各種類型的分拆函數的性質及其相互關係。早在中世紀,就有關於特殊的整數分拆問題的研究。18世紀40年代,L.歐拉提出了用母函數法(或稱形式冪級數法)研究整數分拆,證明了不少有重要意義的定理,為整數分拆奠定了理論基礎。解析數論中的圓法的引進,使整數分拆理論得到了進一步發展。整數分拆與模函數有密切關係,並在組合數學、羣論、概率論數理統計學及質點物理學等方面都有重要應用。
中文名
整數分拆
外文名
Integer splitting、Partition
科    目
數學
應    用
研究各種分拆函數性質及相互關係
提出者
萊昂哈德·歐拉(Leonhard Euler)
相關人物
G.H.哈代,斯里尼瓦瑟·拉馬努金

整數分拆原理

整數分拆問題是一個古老而又十分有趣的問題。所謂整數的分拆 [1]  ,指將一個正整數表示為若干個正整數的和。

整數分拆分類

根據是否考慮分拆部分之間的排列順序,我們可以將整數分拆問題分為有序分拆(composition)和無序分拆(partition)。兩者之間的區別如下:
在有序分拆中,考慮分拆部分求和之間的順序。假定
之間不同的排序記為不同的方案,稱之為n的有序k拆分,如3的有序2拆分為:3=1+2=2+1。我們可以將這個問題建模為排列組合中的“隔板”問題,即n個無區別的球分為r份且每份至少有一個球,則需要用r-1個隔板插入到球之間的n-1個空隙,因此總共的方案數為C(n-1,r-1)。
在無序拆分中,不考慮其求和的順序。一般假定
,我們稱之為n的無序k拆分,如3的無序k拆分為:3=1+2。這種拆分可以理解為將n個無區別的球分為r份且每份至少有一個球。
一般情況下,無序拆分的個數用 p(n) 表示,則 p(2)=1,p(3)=2,p(4)=4。
在通常情況下,整數分拆是指整數的無序分拆。

整數分拆例子

例1 有1克、2克、3克、4克的砝碼各一枚,問能稱出多少重量,並各有幾種稱法。
該問題可以看成n拆分成1,2,3,4之和且不允許重複的拆分數,利用母函數計算如下:
表示稱出4克有2種方案,是1+3和2+2,以此類推,超出10克便無法稱出。
例2 將14分拆成兩個自然數的和,並使這兩個自然數的積最大,應該如何分拆?分析與解 不考慮加數順序,將14分拆成兩個自然數的和,有1+13,2+12,3+11,4+10,5+9,6+8,7+7共七種方法。經計算,容易得知,將14分拆成7+ 7時,有最大積7×7=49。
例3 將15分拆成兩個自然數的和,並使這兩個自然數的積最大,如何分拆?
分析與解 不考慮加數順序,可將15分拆成下列形式的兩個自然數的和:1+14,2+13,3+12,4+11,5+10,6+9,7+8。顯見,將15分拆成7+8時,有最大積7×8=56。
注:從上述兩例可見,將一個自然數分拆成兩個自然數的和時,如果這個自然數是偶數2m,當分拆成m+m時,有最大積m×m=m2;如果這個自然數是奇數2m+1,當分拆成m+(m+1)時,有最大積m×(m+1)。
例4 將14分拆成3個自然數的和,並使這三個自然數的積最大,如何分拆?
分析與解 顯然,只有使分拆成的數之間的差儘可能地小(比如是0或1),這樣得到的積才最大。這樣不難想到將14分拆成4+5+5時,有最大積4×5×5=100。

整數分拆拆分數估計

歷史上,有很多關於整數拆分的研究,其中包括英國劍橋大學的G.E哈代和印度學者拉馬努金,拉馬努拉和哈代通過自己的研究,找到了一個相關的漸進的公式,描述如下。
正整數n拆分成若干個正整數之和,其不同的拆分數用p(n)表示,{p(n)}的母函數為:
拆分數估計可以表示為:

整數分拆拆分數估計定理證明

根據馬克羅林級數:
所以:
又由於
所以
所以
所以
曲線 y = lnx 是向上凸的,所以曲線 y = lnx 在(1,0)的切線為 y = x-1 ,即有
所以
對於
求極小值:
因為
,由均值不等式,得
所以

整數分拆拆分數性質

整數分拆性質一

整數n拆分成最大數為k的拆分數,和數n拆分成k個數的和的拆分數相等。

整數分拆性質二

整數n拆分成最多不超過m個數的和的拆分數,和n拆分成最大不超過m的拆分數相等。

整數分拆性質三

整數n拆分成互不相同的若干奇數的和的拆分數,和n拆分成自共軛的Ferrers圖像的拆分數相等。

整數分拆拆分數計算方法

整數拆分可以利用漸進公式計算,隨着N的無限大,整數拆分估算值接近實際值,現代還可以利用計算機的方法進行求解。在這裏,主要介紹4中計算方法。

整數分拆遞推法

根據n和m的關係,考慮下面幾種情況:
(1)當n=1時,不論m的值為多少(m>0),只有一種劃分,即{1};
(2)當m=1時,不論
的值為多少(n>0),只有一種劃分,即{1,1,....1,1,1};
(3)當n=m時,根據劃分中是否包含n,可以分為兩種情況:
(a)劃分中包含n的情況,只有一個,即{n};
(b)劃分中不包含n的情況,這時劃分中最大的數字也一定比n小,即n的所有(n-1)劃分;
因此,f(n,n) = 1 + f(n, n - 1)。
(4)當n<m時,由於劃分中不可能出現負數,因此就相當於f(n,n);
(5)當n>m時,根據劃分中是否包含m,可以分為兩種情況:
(a)劃分中包含
的情況,即{m,{x1,x2,x3,...,xi}},其中{x1,x2,x3,...,xi}的和為n-m,可能再次出現m,因此是(n-m)的m劃分,因此這種劃分個數為f(n-m,m);
(b)劃分中不包含m的情況,則劃分中所有值都比m小,即n的(m-1)劃分,個數為f(n,m-1);
因此,f(n,m)=f(n-m,m)+f(n,m-1) 。
綜合以上各種情況,可以看出,上面的結論具有遞歸定義的特徵,其中(1)和(2)屬於迴歸條件,(3)和(4)屬於特殊情況,而情況(5)為通用情況,屬於遞歸的方法,其本質主要是通過減少n或m以達到迴歸條件,從而解決問題。
詳細遞推公式描述如下:
遞推公式 遞推公式
參考源碼
#include <stdio.h>

#define  MYDATA long long
const MYDATA MOD = 1000000007;
#define MAXNUM 100005            //最高次數  

//遞歸法求解整數劃分
unsigned long GetPartitionCount(int n, int max)
{
    if (n == 1 || max == 1)return 1;
    if (n < max)return (GetPartitionCount(n, n)) % MOD;
    if (n == max)return (1 + GetPartitionCount(n, n - 1)) % MOD;
    else return (GetPartitionCount(n - max, max) + GetPartitionCount(n, max - 1)) % MOD;
}


int main(int argc, char **argv)
{
    int n;
    int m;
    unsigned long count;
    while (1)
    {
        scanf("%d", &n);
        if (n <= 0)
            return 0;
        m = n;
        count = GetPartitionCount(n, m);
        printf("%d\n", count);
    }
    return 0;
}

這個版本的時間複雜度較高,運行效率很低。

整數分拆動態規劃法

考慮到在上一節使用遞歸中,很多的子遞歸重複計算。如在計算 f (10,9)時,劃分成為 f (1,9) + f (10,8),進一步劃分為 f (1,1)=f (2,8)+f (10,7),接下來轉換為f (1,1) +f (2,2)+ f (3,7)+ f (10,6) =3* f (1,1) +1+ f (3,7)+ f (10,6),這樣就產生了重複,然而,在遞歸的時候,每個都需要被計算一遍,因此可見,位於底層的狀態,計算的次數也越來越多。這樣在時間開銷特別大,是造成運算慢的根本原因,比如算120的時候需要3秒中,計算130的時候需要27秒鐘,在計算機200的時候....計算10分鐘還沒計算出來。
鑑於此,我們已經分析出了普通遞推關係中存在大量的冗餘造成了重複計算,最終導致了運行時間太長,一種自然地想法是能夠用一種技巧,以避免重複計算?這裏我們使用動態規劃的思想進行程序設計。
在上一節中,已經分析了狀態轉移的方程,因此,我們在求解子問題時,利用標記的思想,首先檢查產生的子問題是否在之前被計算過,這是因為對於相同的子問題,得到的結果肯定是一樣的,因此利用這一步去掉冗餘的操作,極大減少了計算的時間開銷。對於沒有標記的子問題來説,一定是沒有被計算過,這樣,在計算完成後,順便標記一下子問題,以確保以後不用再重複計算。利用這個特性,可以確保所有的劃分子問題都無重複,無遺漏的恰巧被計算一次。
動態規劃版主要是利用了標記思想進行加速。
參考源碼如下:
#include <iostream>

#define  MYDATA long long
const MYDATA MOD = 1000000007;
#define MAXNUM 1005            //最高次數  

unsigned  long ww[MAXNUM * 11][MAXNUM * 11];
unsigned long dynamic_GetPartitionCount(int n, int max);

using namespace std;

int main(int argc, char **argv)
{
    int n;
    int m;
    unsigned long count;

    while (1)
    {
        cin >> n;
        cout << dynamic_GetPartitionCount(n, n) << endl;
    }

    return 0;
}

unsigned long dynamic_GetPartitionCount(int n, int max)
{
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
        {
            if (j == 1 || i == 1)
            {
                ww[i][j] = 1;
            }
            else
            {
                if (j == i)
                    ww[i][j] = (ww[i][j - 1] + 1) % MOD;
                else if ((i - j) < j)
                    ww[i][j] = (ww[i - j][i - j] + ww[i][j - 1]) % MOD;
                else
                    ww[i][j] = (ww[i - j][j] + ww[i][j - 1]) % MOD;
            }
        }
    return ww[n][max];
}

這樣的算法效率快了很多。

整數分拆生成函數法

對於整數拆分問題,也可以從另一個角度,即“母函數”的角度來考慮這個問題。所謂母函數,即為關於x的一個多項式
,滿足:
則我們稱為序列
的母函數。我們從整數劃分考慮,假設的某個劃分中,1的出現個數記為
,2的個數記為
,…,
的個數
記為顯然有:
因此
的劃分數,也就是從1到
個數字的組合,每個數字理論上可以無限重複出現,即個數隨意,使它們的和為
。顯然,數字
可以有如下可能,出現0次(即不出現),1次,2次,…,
次等等。把數字用
表示,出現
次的數字用
表示,不出現用1表示。例如,數字2用
表示,2個2用
表示,3個2用
表示,k個2用
表示。則對於從1到
的所有可能組合結果我們可以表示為:
上面的表達式中,每個括號內的多項式代表了數字的參與到劃分中的所有可能情況。因此,該多項式展開後,由於
,因此
就代表了
的劃分,展開後項的係數也就是的所有劃分個數,即
由此我們找到了關於整數劃分的母函數
,剩下的問題就是,我們需要求出
的展開後的所有係數。為此,我們首先要做多項式乘法,對於計算機編程來説,並不困難。我們把一個關於
的多項式用一個整數數組
表示,
代表
的係數,然後卷積即可。
參考源碼:
#include <iostream>
#include <string.h>
#include <stdio.h>

using namespace std;
#define  MYDATA long long
const MYDATA MOD = 1000000007;
const int N = 100005;

int c1[N], c2[N];

int main(int argc, char** argv)
{
    int n, i, j, k;
    while (cin >> n)
    {
        if (n == 0) break;
        for (i = 0; i <= n; i++)
        {
            c1[i] = 1;
            c2[i] = 0;
        }
        for (i = 2; i <= n; i++)
        {
            for (j = 0; j <= n; j++)
                for (k = 0; k + j <= n; k += i)
                    c2[k + j] = (c2[k + j] + c1[j]) % MOD;
            for (j = 0; j <= n; j++)
            {
                c1[j] = c2[j];
                c2[j] = 0;
            }
        }
        cout << c1[n] << endl;
    }
    return 0;
}

這樣基於生成函數的方法實際上快了很多。

整數分拆五邊形數法

設第n個五邊形數為
,那麼
,即序列為:1, 5, 12, 22, 35, 51, 70, ...
對應圖形如下:
五邊形數法 五邊形數法
設五邊形數的生成函數為,那麼有:
以上是五邊形數的情況。下面是關於五邊形數定理的內容:
五邊形數定理是一個由歐拉發現的數學定理,描述歐拉函數展開式的特性。歐拉函數的展開式如下:
即:
可見,歐拉函數展開後,有些次方項被消去,只留下次方項為1, 2, 5, 7, 12, ...的項次,留下來的次方恰為廣義五邊形數。
五邊形數和分割函數的關係:歐拉函數的倒數是分割函數的母函數,亦即:
其中
的分割函數。上式配合五邊形數定理,有:
時,等式右側的係數均為0,比較等式二側的係數,可得
五邊形數法 五邊形數法
因此可得到分割函數的遞歸式:
所以,通過上面遞歸式,我們可以很快速地計算的整數劃分方案數了。
參考代碼:
#include <iostream>

using namespace std;

#define  MYDATA long long
const MYDATA MOD = 1000000007;

#define  AMS 100005
MYDATA pp[AMS];
MYDATA asist[2 * AMS];
void myinit()
{
    for (int i = 0; i < AMS; i++)
    {
        /*算出五角數(正常五角數為1,5,12,22.... k*(3*k-1)/2)*/
        /*此部分,需要算出的是分割函數(1,2,5,7,12,15,22,26.....
        [k*(3*k-1)/2,k*(3*k+1)/2 ])*/
        asist[2 * i] = i * (i * 3 - 1) / 2;
        asist[2 * i + 1] = i * (i * 3 + 1) / 2;
    }
}
void mymethod()
{
    pp[1] = 1;
    pp[2] = 2;
    pp[0] = 1;
    for (int i = 3; i < AMS; i++)
    {
        int k = 0;
        int flags;
        pp[i] = 0;
        /*pp[n]=pp[n-1]+pp[n-2]-pp[n-5]-pp[n-7]+pp[12]+pp[15]
        -.... ..... [+pp[n-k*[3k-1]/2]  + pp[n-k*[3k+1]/2]]*/
        for (int j = 2; asist[j] <= i; j++)
        {
            /*説明:式子中+MOD是必須的,否則輸出可能出錯(有可能為負數)*/
            flags = k & 2;
            if (!flags)
                pp[i] = (pp[i] + pp[i - asist[j]] + MOD) % MOD;
            else
                pp[i] = (pp[i] - pp[i - asist[j]] + MOD) % MOD;
            k++;
            k %= 8;
        }
    }
}

int main(int argc, char** argv)
{
    int  n;
    myinit();
    mymethod();

    while (1)
    {
        cin >> n;
        cout << pp[n] << endl;
    }
    return 0;
}

以上設計的代碼是最高效的。
通過比較上述四種算法,可見整數拆分的劃分方法比較多樣,且不同的算法運行效率差距比較大,需要仔細理解和思考。
參考資料
  • 1.    盧開澄.組合數學(第四版).北京:清華大學出版社,2010