1. 程式人生 > >普通母函式的原理及實現

普通母函式的原理及實現

在用到母函式之前啊,恐怕很少有人聽過母函式,我也一樣,我當時是做杭電acm2082題要用母函式做才去瞭解母函式的。當然母函式分為很多種,包括普通母函式、指數母函式、L級數、貝爾級數和狄利克雷級數,我這裡說的都是普通母函式。

什麼是普通母函式呢,——把組合問題的加法法則和冪級數的的乘冪的相加對應起來,這句話可能一開始難以理解,不過其實學完了之後很容易理解,母函式的思想很簡單就是把離散數列和冪級數一一對應起來,把離散數列間的相互結合關係對應成為冪級數間的運算關係,最後由冪級數形式來確定離散數列的構造

我就從那個經典的砝碼的例子講起。

題目:1克、2克、3克、4克的砝碼各一 枚,能稱出哪幾種重量?每種重量各有幾種可能方案?

窮舉的話很容易就可以得出結果,但是很顯然這種型別的窮舉需要花費的時間是n4方級別,計算機還可以在比較短的而時間內算出來,但是如果給出m種砝碼,那麼就是nm次方級別,那麼如果砝碼種類一多,計算機恐怕就無法在短時間內給出答案。而通過用母函式可以把這類題的時間規模壓縮到n3級別,使得計算機能很快給出解答。

我們先來看看窮舉的過程,先假設使用一個1g的砝碼,然後再假設在此情況下,使用2g的砝碼,在此基礎上,再假設使用一個3g的,再假設使用一個4g的,這就是1中情況,顯然,這種方案稱出的重量為10g,然後改變上面的一個假設條件,比如假設沒有使用4g的,只是用了1g2g3g,顯然這種方案稱出的是6g,以此類推,將所有的可能列舉出來後即可得到全部的方案,

1g的砝碼有兩種情況,使用還是不使用,其他的砝碼也一樣,每一種1g砝碼的狀態都可以有兩種砝碼狀態,以此類推總共就有2*2*2*2=16中方案,當然其中包含一種一個砝碼也沒有的0g方案。

我們最後可以窮舉出結果:1g2g8g9g10g各有一種方案,3g4g5g6g7g各有兩種,總共十五種,這是在不算一種0g的方案的時候,算上就是16種,和上面的過程相符。

我們可以用一個類似離散數學的邏輯式表示前兩種砝碼組合出來的情況

我在這裡使用||表示析取,也就是程式設計時的“邏輯或”;用&&表示合取,也就是程式設計時的“邏輯與”,析取與合取都是離散數學中的概念,其實就等於程式語言中的邏輯與和邏輯或,析取與合取的符號ˆ和ˇ很多人雖學過並不熟悉,我就用程式語言中的符號代替了,熟悉離散數學的人可以轉換成析取與合取的符號。

(使用1g || 不使用1g&&(使用2g || 不使用2g

=使用1g&&使用2g || 不使用1g&&使用2g || 使用1g&&不使用2g || 不使用1g&&不使用2g

每個用 “||”分開的一項都是一種方案。“&&”表示選擇第一項的情況下有選擇了第二項。

大家可以發現這個表示式和一種表示式很像,沒錯,如果把“||”看成加法,“&&”看成乘法,和多項式的乘法一模一樣。那麼我們直覺的想到,有沒有可能用多項式乘法來表示組合的情況呢?我們再來看題目,題目需要的是幾種砝碼組合後的重量,是一個加法關係,但是在上式中“&&”是一種類似於乘法的運算關係,這怎麼辦呢?有沒有什麼這樣一種運算關係,以乘法的形式運算,但是結果表現出類似於加法的關係呢?正好有一個,那就是冪運算。X乘上Xn結果是Xm+n,他完美的符合了我們的要求。那麼以次數表示砝碼的質量, 就可以以多項式的形式表示砝碼組合的所有方案。

還是以前兩個砝碼為例說明,表示1g砝碼的兩種狀態的多項式就是(x0+x1),表示2g的就是(x0+x2),x0表示沒有使用砝碼,所以重量為0,當然了因為x0 =1,所以也可以寫成1,後面為了方便我會這麼寫。注意,砝碼的質量是以次數表示的,而不是直覺上的用下標表示為x1x2。這點很重要,不然的話就無法表現出冪運算的關係了。

x0+x1*x0+x2

=x0*x+ x1*x0 + x0*x+ x1*x2

=x+ x1 + x+ x3

很顯然,有四種方案,0g1g2g3g,結果與我們窮舉的結果相同,而如果結果中有相同的項,那麼合併同類項後每一項的係數就是這種重量有幾種實現方案,這會在我們用此方法解決4個砝碼問題的時候得到證明。

那麼接下來試試用這種表示式表示4個砝碼的組合情況

x0+x1x0+x2) x0+x3x0+x4

=x+ x1 + x+ 2x3 + 2x+ 2x5 + 2x+ 2x+ x8+ x9 + x10 

具體的計算過程我就不細寫了,多項式乘法大家都應該會。

結果與上面窮舉的結果是相同的,次數代表組合後可稱出的質量,係數代表組合出這種質量的方案的數量。至此也就得出了答案。這就是普通母函式,現在你可以回頭去看看我前面說的那兩句話——把組合問題的加法法則和冪級數的的乘冪的相加對應起來把離散數列間的相互結合關係對應成為冪級數間的運算關係,最後由冪級數形式來確定離散數列的構造。是不是明白了母函式的本質了呢?是不是恍然大悟了呢?

接下來我們把開始的題目稍稍變化一下:

求用1g2g3g砝碼稱出不同重量的方案數

大家把這種情況和第一種比較有何區別?第一種每種是一個,而這裡每種是無限的。

怎樣在母函式裡表現出無限這種性質呢?很簡單,我們以2g砝碼為例,因為我們有無限個2g砝碼,所以我們可以把22g砝碼看成是4g砝碼,32g砝碼看成是6g砝碼,依次類推,把mn g砝碼看成是一個n*mg砝碼,還是先以前兩個砝碼為例,那麼多項式相應的就變成

x+x+ x+ x+ x+ x…… )*x+ x+ x+ x+ x8+ x10 ……)

結果自然也是無限的,但是這種問題在實際問題中,一定會給出一個確定的值,比如說求組成10g的方案有幾種。那麼我們就只要在合併後的結果求到最高次的項是x10即可,後面的項可以忽略不計。那麼要結果中最高為10次方,開始每一種砝碼的無限項的表示式該寫到幾次呢?也是10次,因為表示式中最低的項有x,所以想在結果中不漏掉出現x10 的項,必須乘之前的項最高的項不能小於x10,而表示式中不可能出現x-1,,所以x11 和任何一項相乘都會大於x10,所以x11 是不需要的,但寫上也無妨,不影響結果,但是如果可以有x10(比如3g的砝碼組合不出10g的,但是2g的就可以),那就必須寫,不然就會漏掉一些方案。

那麼如果題目是求用1g2g3g砝碼稱出10g的方案數

表示式就是:

x+x+ x+ x+ x+ x……x10 )*x+ x+ x+ x+ x8+ x10 

結果就是合併同類項後x10的係數。

多項式乘法顯然是一種運算時間規模在n2級別的運算,但是如果迴圈生成無限的砝碼,也就是上面用小砝碼組合出每一種可能的大砝碼,那麼生成多項式就又需要n的規模,在此基礎上進行多項式乘法,最後無限砝碼的問題需要的運算時間規模就是n3級別。

這樣母函式就把這類組合問題從nn級別轉化成了n3級別。這便是母函式的奇妙與威力。

知道了母函式的原理,用程式來實現也就不是什麼困難的事了,其實就是做多項式乘法的程式

c++實現算出無限砝碼情況下的某種重量的方案數。

#include <iostream>  
using namespace std;  
// Author: bjr  
//   
const int max = 1000;   
// sup是儲存多項式的陣列,sup[n]中的值代表xn的係數
// temp是臨時多項式,儲存相乘的臨時中間情況  
int sup[max], temp[max];   
/*
程式始終只計算兩個多項式之間的乘積,多個多項式的情況
先計算前兩個的乘積,將結果作為第一個多項式,再與第三個相乘
依次類推,sup始終存放當前運算後的結果然後作為被乘多項式,
*/  
int main()  
{   
    int target;   //  目標重量, 比如上面的例子裡就是10,要<max的值
    int i, j, k;  
   
    while(cin >> target)  
    {  
        for(i=0; i<=target; ++i)     
        {  
            sup[i] = 1;   
//初始化第一個多項式,也就是用1g砝碼的多項式,
//注意如果題目沒給1g的砝碼那麼就不能++i,而要加上砝碼的質量
            temp[i] = 0;  
//將臨時區清空,無論第一個多項式質量是幾都要全部置零
        }  
        for(i=2; i<=target; ++i)   
// 生成後續的第i個多項式,此題中是2g,i從2開始。
//如果砝碼的值不是規律增長,i可能需要取決於輸入
        {  
   
            for(j=0; j<=target; ++j)   
// 遍歷當前結果多項式的每一項(當前結果的第j項)與第i個多項式相乘,
                for(k=0; k+j<=target; k+=i) 
// 遍歷第i個多項式的每一項,此處構造用小砝碼組成大砝碼的多項式
                {  
                    temp[j+k] += sup[j];  
//冪運算,注意理解
                }  
            for(j=0; j<=target; ++j)    
// 將臨時的結果覆蓋當前結果,同時把臨時結果置零,為下次做準備
            {  
                sup[j] = temp[j];  
                temp[j] = 0;  
            }  
        }  
        cout << sup[target] << endl;  //輸出結果
    }  
    return 0;  
}