代码改变世界

妙用生成函数

2015-04-15 23:37  星星之火✨🔥  阅读(617)  评论(0编辑  收藏  举报

还是从一道题目来开始生成函数的研究吧,下面的题目取自HDU_1028:

Problem Description
"Well, it seems the first problem is too easy. I will let you know how foolish you are later." feng5166 says. "The second problem is, given an positive integer N, we define an equation like this:  
N=a[1]+a[2]+a[3]+...+a[m];  
a[i]>0,1<=m<=N;
My question is how many different equations you can find for a given N. For example, assume N is 4, we can find:  
4 = 4;  
4 = 3 + 1;
4 = 2 + 2; 
4 = 2 + 1 + 1;  
4 = 1 + 1 + 1 + 1;
so the result is 5 when N is 4. Note that "4 = 3 + 1" and "4 = 1 + 3" is the same in this problem. Now, you do it!"
Input
The input contains several test cases. Each test case contains a positive integer N(1<=N<=120) which is mentioned above. The input is terminated by the end of file.
Output
For each test case, you have to output a line contains an integer P which indicate the different equations you have found.
Sample Input
4
10
20
Sample Output
5
42
627
可以这样考虑,一个自然数的一个可能的组合形式可以包含任意个1,任意个2,任意个3,... 等等组成。
因此我们有G(x) = (1+x^1+x^2+x^3+...)(1+x^2+x^4+x^6)...(1+x^n+x^2n+...),该公式中的1可以看成x^0,不难得出,该生成函数展开后的形式中每一项的幂就代表一个自然数,其前的系数代表该自然数的组合形式有多少,我们模拟手工计算的方式写出程序:
#include<stdio.h> 
#include<string.h>
int main(void)
{
    int arr[130];
    int temp[130];
    int n;
    while(scanf("%d", &n) != EOF)
    {
        for(int i = 0; i <= n; i++) // 初始化第1个表达式的系数;任何自然数都可以表示成若干个1的和
        {
            arr[i] = 1;
            temp[i] = 0;
        }
        for(int i = 2; i <= n; i++) // 从第2个表达式开始到第n个表达式
        {
            for(int j = 0; j <= n; j++) // 最前面表达式的各个系数为arr[i]
{ for(int k = 0; k+j <= n; k += i) // 最前面的下一个表达式,指数增量为i temp[k+j] += arr[j]; // 指数为j+k的系数为arr[k+j]加上最前面的那个表达式的指数为j的系数(即temp[j]) } for(int i = 0; i <= n; i++) // 前i个表达式的乘积系数 { arr[i] = temp[i]; temp[i] = 0; // 临时数组清零以备下次使用 } } printf("%d\n", arr[n]); } return 0; }
注意到这个程序完全模拟了手工计算的过程,一个接一个的表达式相乘(排在最前面的那两个),并且忽略了我们不关心的部分,即大于n的情况没有计算。另外,测试可知,int型能放得下n = 120 的情况,因此我们可以不用long long int。额外的,如果在线评测系统测试数据量很大的话,上面的程序由于重复计算可能造成超时,因此可以采用预处理的方式,俗称打表法:
#include<stdio.h> 
#include<string.h>
#define MAXN 120
int main(void)
{
    int arr[130];
    int temp[130];
    int n;
    
    for(int i = 0; i <= MAXN; i++) 
    {
        arr[i] = 1;
        temp[i] = 0;
    }
    for(int i = 2; i <= MAXN; i++) 
    {
        for(int j = 0; j <= MAXN; j++) 
        {
            for(int k = 0; k+j <= MAXN; k += i) 
                temp[k+j] += arr[j]; 
        }
        for(int i = 0; i <= MAXN; i++) 
        {
            arr[i] = temp[i]; 
            temp[i] = 0;
        }
    }
    
    while(scanf("%d", &n) != EOF)
    {
        printf("%d\n", arr[n]);
    }
    
    return 0;
}
为了充分理解上面代码的含义,我们故技重施,再取一题,HDU_1398:
Problem Description
People in Silverland use square coins. Not only they have square shapes but also their values are square numbers. Coins with values of all square numbers up to 289 (=17^2), i.e., 1-credit coins, 4-credit coins, 9-credit coins, ..., and 289-credit coins, are available in Silverland.  There are four combinations of coins to pay ten credits: 
ten 1-credit coins,
one 4-credit coin and six 1-credit coins,
two 4-credit coins and two 1-credit coins,
and one 9-credit coin and one 1-credit coin. 
Your mission is to count the number of ways to pay a given amount using coins of Silverland.
Input
The input consists of lines each containing an integer meaning an amount to be paid, followed by a line containing a zero. You may assume that all the amounts are positive and less than 300.
Output
For each of the given amount, one line containing a single integer representing the number of combinations of coins should be output. No other characters should appear in the output. 
Sample Input
2
10
30
0
Sample Output
1
4
27

同样地,我们写出其生成函数G(x) = (1+x^1+x^2+x^3+...)(1+x^4+x^8+x^12+...)...(1+x^298+x^596+...),写程序来模拟手工计算过程如下:

#include<stdio.h> 
int main(void)
{
    int ans[310];
    int temp[310];
    int n;
    while(scanf("%d", &n) && n!=0)
    {
        for(int i = 0; i <= n; i++)
        {
            ans[i] = 1;
            temp[i] = 0;
        }
        for(int i = 2; i*i <= n; i++) // 另一个注意点,如果i <= n 的话,由于循环次数增多31MS才AC(i*i <= n 0MS_pass)
        {
            for(int j = 0; j <= n; j++)
            {
                for(int k = 0; k+j <= n; k += i*i) // 一个注意点
                    temp[k+j] += ans[j];
            }
            for(int i = 0; i <= n; i++)
            {
                ans[i] = temp[i];
                temp[i] = 0;
            }
        }
        printf("%d\n", ans[n]);
    }

    return 0;
}

打表法和上一题的程序中的打表法完全相同,不再赘述。

此外,我有个疑惑,为什么程序中嵌套层次最深的那个语句temp[k+j] += ans[j] 不能直接用ans[k+j] += ans[j]来代替?从而省略那个辅助数组temp?然而替换掉之后,输出结果的对应项的系数普遍都大了,这是为什么?其实本质上还是模拟计算考虑的不够细致,为什么不能省略辅助数组?经过一夜的精致睡眠后:-,最终还是想通了,这是因为每次后面的表达式和前面的表达式相乘的时候,都需要依次取出后面表达式的每一个元素和前面表达式的某个元素的系数相乘,在这两个表达式相乘的过程中,注意到前面表达式各项的系数是不能变的,需要固定住的,因此需要一个额外的数组来保存各项的系数。

最后,话题还是回到生成函数上来吧,考虑下面的问题:

把价值1 美元、2 美元、和5 美元的货币插入售货机为价值为r 美元的某种物品付款,使用生成函数确定在货币插入是有序的和无序的两种情况下付款的方式数。(例如为一种价值为3 美元的物品付款,当不考虑货币插入的次序时存在2 种方式:插入3 个1 美元的货币或1 个1 美元和1 个2 美元的货币。当考虑货币插入的次序时有3 种方式:插入3 个1 美元的货币,插入1 个1 美元的货币然后1 个2 美元的货币,插入1 个2 美元货币然后1 个1 美元货币。)

在不考虑货币插入次序的情况下,我们所关心的就是为产生r 美元的总数所使用的每种货币的数目。因为可以使用任意多个1 美元的货币、任意多个2 美元的货币和任意多个5 美元的货币,答案就是生成函数(1+x+x^2+x^3+...)(1+x^2+x^4+...)(1+x^5+x^10+...) 展开式中的x^r的系数(乘积中的第一个因式表示所使用的1 美元货币,第二个表示所使用的2 美元的货币,第三个表示所使用的5 美元的货币)。例如,用1 美元、2 美元和5 美元为一个价值为7 美元的物品付款的方式数由展开式中x^7 的系数给出,结果等于6。

当考虑货币插入的次序是,插入恰好n 个货币产生r 美元总数的方式数是在(x+x^2+x^5)^n 的展开式中的x^r 的系数,因为这n 个货币中的每一个可能是1 美元货币、2 美元货币或5 美元货币。

综上(1+x+x^2+x^3+...)(1+x^2+x^4+...)(1+x^5+x^10+...) +  (x+x^2+x^5)^n 的展开式中x^r 的系数就是问题的解。我们可以用计算机模拟计算的方式求解这个问题。

All Rights Reserved.
Author:海峰:)
Copyright © xp_jiang. 
转载请标明出处:http://www.cnblogs.com/xpjiang/p/4430256.html
以上.