生成函數

回顧了以前的一些沒有完成的題目,發現自己的知識漏洞還挺多的,比如這個生成函數【以前用多重背包一直WA】

生成函數干嘛的?

比較功利的來說,就是用來求解各種排列組合的問題。如果是求組合問題,那就是構造普通型生成函數,如果是求排列的問題,那就是構造指數型生成函數。

最關鍵的是,這個不像DP,這個是有固定的模板的~那就很簡單了。以下是我找的一些生成函數的題目,基本都是一個思想,主要就是通過例子來熟練使用生成函數

普通型生成函數

假設你手里有1張1塊,1張2塊,1張5塊的紙幣,能夠組成多少種數量的錢,每種數量的錢有幾種組成方案

1塊 -> 金額1
2塊 -> 金額2
5塊 -> 金額5
1塊 + 2塊 -> 金額3
1塊 + 5塊 -> 金額6
2塊 + 5塊 -> 金額7
1塊 + 2塊 + 5塊 -> 金額8
(省略了一張都不用,金額0的情況)

設有函數:G(x) = a_0x^0 + a_1x^1 + a_2x^2 + …… + a_nx^n

其中,x2表示金額2的情況,x5表示金額5的情況,而前面的系數a2表示金額2的組成方案數量,a5表示金額5的組成方案數量

所以剛才舉例中可得到的函數是:
G(x) = x^0 + x^1 + x^2 + x^3 + x^5 + x^6 + x^7 + x^8

把這個函數分解開可以得到

  • 只有一張1塊的生成函數 G(x) = x0 + x1 【可以有0塊和1塊這兩種金額】
  • 只有一張2塊的生成函數 G(x) = x0 + x2

把這兩個式子相乘可以得到1塊和2塊的生成函數:
(x^0 + x^1)×(x^0 + x^2) = x^0 + x^1 + x^2 + x^3

  • 1塊和2塊的生成函數 G(x) = x0 + x1 + x2 + x3
  • 只有一張5塊的生成函數 G(x) = x0 + x5

把這兩個式子相乘可以得到1塊、2塊和5塊的生成函數:
(x^0 + x^1 + x^2 + x^3) × (x^0 + x^5) = x^0 + x^1 + x^2 + x^3 + x^5 + x^6 + x^7 + x^8

組合問題其實就是與非運算的加法問題。比如上述方案中,就是1、2、5都要;1、2要,5不要;1、5要,2不要;……構成的方案,而這種運算方式恰好可以與冪級數的乘積對應起來

HDU2082

26個字母價值各不相同,且每個字母的數量不一,最后問能組成多少種總價值為50的單詞的方案

在上面的例子中,只用到了一張1塊,1張2塊和1張5塊。假設有2張2塊,那么就應該能組成0、2、4三種金額;假如有3張2塊,那么就能組成0,2,4,6四種金額。也就是說,3張2塊的生成函數為:
x^0 + x^2 + x^4 + x^6

如果數量不限,那么無數張2塊的生成函數為:
x^0 + x^2 + x^4 + …… + x^{2n}

在這個基礎之上,這類題目其實就變成了多項式的乘積問題。在實際編碼時,多項式用數組表示,數組的下標就是對應的多項式的次數。

首先初始化三個數組,a數組記為目前計算完成的生成函數,b數組是當前正在計算的生成函數,將a數組和b數組進行多項式的乘積操作,將計算的答案存儲到c數組中,最后將c數組拷貝到a數組中,更新生成函數,具體的編碼過程如下:

memset(a,0,sizeof(a));
memset(c,0,sizeof(c));
a[0] = 1;
for (int k=1; k<=26; k++) {
    memset(b,0,sizeof(b));
    cin>>num;
    if (num==0) continue;
    for (int i=0; i<=num && k*i<=50; i++)
        b[i*k] = 1; //當前價值的生成函數,對應倍數的系數置1
    for (int i=0; i<=50; i++) 
        for (int j=0; j<=50 && i+j<=50; j++)
            c[i+j] += a[i]*b[j];  //i+j表示冪指數相加,a[i]*b[j]表示系數相乘
    for (int i=0; i<=50; i++) {  //拷貝c數組
        a[i] = c[i];
        c[i] = 0;
    }
}

具體的思路就是不斷累乘多項式,每個多項式就是每個價值的生成函數,最后得到的就是所有價值的生成函數,也就是所有組成的方案數量,上面題目中,x50的系數就是對應的方案數,也就是a[50]的值

簡化累乘過程

上述代碼中用到了三個數組,其實可以只用兩個數組,因為每一個新的生成函數,系數都是1,比如 4個價值為2 的字母的生成函數:
G(x) = x^0 + x^2 + x^4 + x^6 + x^8
而相乘的過程就是x0乘以數組a中的每一項,x2乘以數組a中的每一項……所以可以得到如下的簡化代碼

for (int i=0; i<=num && k*i<=50; i++)
    //當前指數是i*k,系數是1
    for (int j=0; j<=50 && i*k+j<=50; j++)
        c[i*k+j] += a[j];

其中i*k就是當前價值的生成函數的指數,當字母價值為2時,k=2,i從0到4,也就對應了當前每一項的指數,對于每一項指數都需要乘以已經計算好的a數組中的每一項,而a數組中的每一項的系數就是a[j],指數是j,所以i*k+j就是新的指數,而相應的系數,因為當前系數是1,所以相當于加上a[j]

最后需要求的是小于等于價值為50的單詞的方案總數,那就是a[1]累加到a[50]即可,也就是系數和相加

HDU2110

這里的總數不再是50這個固定的值了,而是所有給出的量的總和除以3,也就是1/3的總資產。

核心代碼還是一樣,只是替換了部分變量而已。下滿的p[i]存儲的是每個資產的價值,m[i]存儲的每個資產的數量,因為上題中資產的價值是固定的,所以這題需要增加數組存儲每個資產的價值和數量

for (int k=1; k<=n; k++) {
    for (int i=0; i<=m[k] && p[k]*i<=total; i++) {
        for (int j=0; j<=total && i*p[k]+j<=total; j++) {
            b[i*p[k]+j] += a[j];
            b[i*p[k]+j] %= 10000;
        }
     }
     for (int i=0; i<=total; i++) {
         a[i] = b[i];
         b[i] = 0;
     }
}
HDU1028

這題和上面兩題不同的是,在這題中,每一個價值的數量是無限的,有無限個1、無限個2……而循環的終點就是目標價值量N

核心代碼與上面兩題基本相同

for (int k=1; k<=N; k++) {
    for (int i=0; k*i<=N; i++)
        for (int j=0; j<=N && i*k+j<=N; j++)
            b[i*k+j] += a[j];
    for (int i=0; i<=N; i++) {
        a[i] = b[i];
        b[i] = 0;
    }
}
HDU1171

這題是大一的時候用多重背包做了很久,一直沒做出來,最后就放著的一題。現在去看看當初寫的代碼量,都快到200行了。

在這題中,目標價值不是固定的了。題意是盡可能的將所有資產平分,那也就是說,目標價值是盡量接近總價值的一半,且存在可能的方案(系數不為0)

核心代碼與HDU2110一樣

for (int k=1; k<=N; k++) {
    for (int i=0; i<=m[k] && i*v[k]<=total; i++)
        for (int j=0; j<=total && i*v[k]+j<=total; j++)
            b[i*v[k]+j] += a[j];
    for (int i=0; i<=total; i++) {
        a[i] = b[i];
        b[i] = 0;
    }
}

在計算完成之后,從中點向起點遍歷,找到第一個系數不為0的指數即可,因為存在只有一種資產的情況,所以遍歷時不能只遍歷到1,而需要遍歷到0,也就是說一方什么都沒分到,另一方拿走全部

int t = total/2;
for (int i=t; i>=0; i--) {  //注意這里是大于等于0,而不是大于0
    if (a[i]!=0) {
        cout<<total-i<<" "<<i<<endl;
        break;
    }
}
HDU2152

在這題中,加上了每個價值量(水果)最少出現的次數和最多出現的次數,且題意中總數不大于100

因為第二個循環代表的是當前價值量的使用次數,所以修改第二層循環即可,另外,由于在題意中,每個水果的價值量單位都是1,所以前面幾題中的p[k]、v[k]都可以省略

for (int k = 1; k<=N; k++) {
    for (int i = mins[k]; i<=maxs[k] && i<=100; i++)
        for (int j = 0; j<=100 && i+j<=100; j++)
            b[i+j] += a[j];
    for (int i=0; i<=100; i++) {
        a[i] = b[i];
        b[i] = 0;
    }
}

以上就是普通型生成函數的例題,通過這幾題,大概能得到一個通用的組合方案數計算的模板,理解之后,修改代碼就方便了

指數型生成函數

上面已經提到,指數型生成函數是用來處理排列問題的。排列問題存在重復的排列項,比如11333,有2個1和3和3,就需要除以重復項2!3!,所以得到指數型的生成函數:

G(x) = a_0 + \frac{a_1}{1!}x + \frac{a_2}{2!}x^2 + \frac{a_3}{3!}x^3 + …… + \frac{a_n}{n!}x^n

假設有3個紅球,2個黃球,4個綠球。

那么我們規定3個紅球的生成函數是:
G(x) = 1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!}

2個黃球的生成函數是:
G(x) = 1 + \frac{x}{1!} + \frac{x^2}{2!}

3個紅球和2個黃球的生成函數是:
(1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!})×(1 + \frac{x}{1!} + \frac{x^2}{2!})

4個綠球的生成函數是:
G(x) = 1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!}

所以:3個紅球、2個黃球、4個綠球的生成函數為:
(1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!})×(1 + \frac{x}{1!} + \frac{x^2}{2!}) × (1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!}) =

1 + 3\frac{x^1}{1!} + 9\frac{x^2}{2!} + 26\frac{x^3}{3!} + 71\frac{x^4}{4!} + 180\frac{x^5}{5!} + 410\frac{x^6}{6!} + 805\frac{x^7}{7!} + 1260\frac{x^8}{8!} + 1135\frac{x^9}{9!}

也就是說,從這些球中,取2個的排列數為9,取3個的排列數為26,去4個的排列數為71……

非固定數量的球的生成函數
  • 無限數量
    G(x) = 1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!} + … + \frac{x^n}{n!} = e^x
  • 奇數個
    G(x) = \frac{x}{1!} + \frac{x^3}{3!} + \frac{x^5}{5!} + … + \frac{x^{2n+1}}{(2n+1)!} = \frac{e^x - e^{-x}}{2}
  • 偶數個
    G(x) = 1 + \frac{x^2}{2!} + \frac{x^4}{4!} + … + \frac{x^{2n}}{2n!} = \frac{e^x + e^{-x}}{2}

具體計算時,按固定數量的一樣,相乘即可,非固定數量可以直接用級數和來代替

HDU1521

這是固定數量的球,我們同樣使用數組來表示多項式,數組下標對應的是指數,數組中的值對應的是系數

double Factorial(int n) { //計算n!
    double ans = 1;
    for (int i=1; i<=n; i++) ans*=i;
    return ans;
}
int main() {
    double a[11],b[11];
    int n,m,num[11];
    while (cin>>n>>m) {
        for (int i=1; i<=n; i++)
            cin>>num[i];
        memset(a,0,sizeof(a));
        memset(b,0,sizeof(b));
        a[0] = 1.0;  //初始化數組為1*x^0 = 1
        for (int k=1; k<=n; k++) {  //計算第k個數
            for (int i=0; i<=num[k]; i++)  //當前計算的數的指數遍歷
                for (int j=0; j<=m; j++) //對于每一個指數,都分別乘以已經計算好的多項式
                    b[j+i] += a[j]/Factorial(i);  //每一個項初始化的都是x^n/n!,計算時,相當于指數相加,系數除以n!
            for(int j=0;j<=m;j++){
                a[j] = b[j];
                b[j] = 0;
            }
        }
        cout<<fixed<<setprecision(0)<<a[m]*Factorial(m)<<endl; //保證無小數輸出
    }
    return 0;
}

我們發現核心代碼還是相同的,由于數組沒項存儲的還是指數對應的系數,而指數是1/n!,所以計算時存在小數,所以用double類型

核心代碼中的計算b[j+i] += a[j]/Factorial(i); ,由于剛初始化時的每一個數的生成函數的每一項的系數都是1/i! (i表示的是當前的指數),所以多項式相乘就相當于指數相加,系數等于原系數除以i!

大致上與普通型生成函數相比,核心代碼還是一致的,只是計算時多出了計算階乘的步驟

HDU2065

這題的與上題相比不同的是,數字(字母)有無限個,出現的次數不限或者限奇數次、偶數次

根據上面的公式,得到無限次的生成函數和偶數次的生成函數分別是:
e^x\frac{e^x + e^{-x}}{2}

而題意中,無限次的有兩個字母,偶數次的也有兩個字母,所以生成函數為:
e^x × e^x × \frac{e^x + e^{-x}}{2} × \frac{e^x + e^{-x}}{2} = \frac{e^{4x}+2e^{2x}+1}{4}

現在我們需要求的是排列的個數是n的時候的方案數量,也就是\frac{x^n}{n!}的系數是多少

我們把e4x和e2x在x=0處進行泰勒展開,變為多項式【總算用到高等數學中的東西了】。展開式就不累贅了,展開后,e4x\frac{x^n}{n!}的系數為4n;e2x\frac{x^n}{n!}的系數為2n

所以,所求的系數為\frac{4^n + 2^{n+1}}{4} = 4^{n-1} + 2^{n-1}

所以只要將n帶入這個式子即可,當然由于題中的n很大,所以要用到快速冪,快速冪在前面的文章中已經有介紹了,矩陣快速冪——入門

所以最后的答案就是(quickPow(4,n-1)+quickPow(2,n-1))%100

POJ3734和上題相同,題意表述不同而已

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容