上篇帖子链接:金山程序题2的优化
照例先给出题目:
给定一个数组大小m和一个数组array
求从array中任意取得n(n<=m)个数,使得和为m,总共有多少种取法.
例如: m = 10; array = {1,2,3,4,5,6,7,8,9,10} 共10种取法
1 2 3 4, 1 2 7, 1 3 6, 1 4 5, 1 9, 2 3 5, 2 8, 3 7, 4 6, 10
方法原型 :int getotalNum (int[] array, int m);
这道题目我从11月11日收到起至今已经过去一个月了,处理进度分三个阶段:
1、11.11 当天利用组合数学的算法得到初步结果,由于优化失误,当时只能正确处理正数
2、12.06 优化了一次,时间复杂度仍然是O(sigma(1,i,n),C(n,i)),但是剪枝效果不错,比第一次效率有了百倍的提升,但是由于没有发现代码中的失误,仍旧不能正确处理负数
3、12.13 根据腌菜同学的建议,参考了Matrix67的资料和腌菜的核心代码,进行了第二次优化。时间复杂度剧烈降低至O(m*n),而且可以正确处理负数情况,是远超上次的巨大提升。
今天这篇日至就是12.13日优化过程中的一些情况和资料的总结。
首先介绍了是本文的重点 生成函数,下面几段摘自Matrix67的博文:什么是生成函数? (有删改)
________________________简易分割线______________________________
我们年级有许多漂亮的MM。一班有7个左右吧,二班大概有4个,三班最多,16个,四班最可怜,一个漂亮的MM都没有,五班据说有1个。如果用一个函数“f(班级)=漂亮MM的个数”,那么我们可以把上述信息表示成:f(1)=7,f(2)=4,f(3)=16,f(4)=0,f(5)=1,等等。
生成函数是说,构造这么一个多项式函数g(x),使得x的n次方系数为f(n)。于是,上面的f函数的生成函数g(x)=7x+4x^2+16x^3+x^5+...。这就是传说中的生成函数了。
生成函数最绝妙的是,某些生成函数可以化简为一个很简单的函数。也就是说,不一定每个生成函数都是用一长串多项式来表示的。比如,这个函数f(n)=1 (n当然是属于自然数的),它的生成函数就应该是g(x)=1+x+x^2+x^3+x^4+...(每一项都是一,即使n=0时也有x^0系数为1,所以有常数项)。再仔细一看,这就是一个有无穷多项的等比数列求和嘛。如果-1
例1:从二班选n个MM出来有多少种选法。学过简单的排列与组合的同学都知道,答案就是C(4,n)。也就是说。从n=0开始,问题的答案分别是1,4,6,4,1,0,0,0,...(从4个MM中选出4个以上的人来方案数当然为0喽)。那么它的生成函数g(x)就应该是g(x)=1+4x+6x^2+4x^3+x^4。这不就是……二项式展开吗?于是,g(x)=(1+x)^4。
我们再举一个例子说明一些更复杂的生成函数。例2:k=x1+x2+x3+...+xn有多少个非负整数解?这道题是学排列与组合的经典例题了。把每组解的每个数都加1,就变成n+k=x1+x2+x3+...+xn的正整数解的个数了。教材上或许会出现这么一个难听的名字叫“隔板法”:把n+k个东西排成一排,在n+k-1个空隙中插入n-1个“隔板”从而把数字分成n块,每块的东西个数为每个自变量的值。这样隔板的放置的方法和解的个数之间形成了一一对应的关系。至于隔板放置的方法我们总是知道的,就是从n+k-1个空隙中无序的找出n-1个用来放置隔板,就是C(n+k-1,n-1)。它就等于C(n+k-1,k)。而它关于n的生成函数是g(x)=C(n+0-1,0)+C(n+1-1,1)x+...+C(n+k-1,k)x^k+.... = 1/(1-x)^n 这个生成函数是如何换算来的呢?
1/(1-x)=1+x+x^2+x^3+x^4+...是前面说过的。我们对这个式子等号两边同时求导数。于是,1/(1-x)^2=1+2x+3x^2+4x^3+5x^4+....。不断地再求导数,得到了这样一个公式:1/(1-x)^n=1+C(n,1)x^1+C(n+1,2)x^2+C(n+2,3)x^3+...+C(n+k-1,k)x^k+...。就是上面我们得到的那个公式。分析这个公式g(x)=1/(1-x)^n=(1+x+x^2+x^3+...)^n,仔细想想n个(1+x+x^2+x^3+...)相乘是什么意思。(1+x+x^2+x^3+...)^n的展开式中,k次项的系数就是我们的答案,因为它的这个系数是由原式完全展开后n个指数加起来恰好等于k的项合并起来得到的。
所以我们总结下例2的规律,x1至xn每个数字都能取从1到k之间的值,所以每个自变量对应的生成函数 g(x)=1+x+x^2+x^3+..+x^n =1/(1-x),而整个题目的生成函数则是,G(x)=1/(1-x)^n=g(x)*g(x)*...*g(x),也就是全部自变量的生成函数的乘积。得到之后总的生成函数之后,计算出对应指数k的系数就是我们题目所要的!下面几个例题,可以理解下加深印象:
例3:我们要从苹果、香蕉、橘子和梨中拿一些水果出来,要求苹果只能拿偶数个,香蕉的个数要是5的倍数,橘子最多拿4个,梨要么不拿,要么只能拿一个。问按这样的要求拿n个水果的方案数。
G(x)=(1+x^2+x^4+...)*(1+x^5+x^10+...)*(1+x+x^2+x^3+x^4)*(1+x)
=...(划减步骤略,套等比数列通相和公式)
=(1-x)^(-2)
=C(1,0)+C(2,1)x+C(3,2)x^2+C(4,3)x^3...
=1+2x+3x^2+4x^3+5x^4+....
指数为n的系数是n+1,故n+1就是我们所求得解。
________________________简易分割线______________________________
以上大部分内容来自于Matrix67的博文,dave为了使大家更好理解删改了一部分内容,这里是原文连接:http://www.matrix67.com/blog/archives/120
下面就结合咱们的程序题目用生成函数的思想来解决这个问题。整理题目如下:
无序整数数列a[0...m-1], 对于数列的任何一个数字可以标记或者不标记,使得标记的数字之和为m。
于是:G(x)=(1+x^a[0])*(1+x^a[1])*..*(1+x^a[m-1]),这里每个多项式退化成二项式,由于我们的数列a是用户输入的,无序的,所以这个式子没有办法用数学方法划减,只能用计算机来处理。
我们可以用一个长m+1的数数组r来存储G(x)从0到m指数的系数。把每个a[ i ]看作每个二项式的指数,依次相乘。但是问题又来了,如果指数a[ i ]是小于0的,怎么办?我们在r数组用下标表示指数,但是下标不能为负数。于是在进行计算前,我们应该把所有小于0的指数都变成非负指数。
例如对于(1+x^-2)我可以让它乘上x^2,变成(x^2+1),可以作为正常的二项式参加运算,G(x)变成G(x)*x^2,而我们要求指数m(=n)的系数,也变成求指数m+2=(n+2)的系数,数组r的长度也必须增加到m+1+2(=n+1)。
对于处理二项式的乘法,由于每个二项式中必定有一个常数项1,用1乘另外一个多项式p,多项式不变p,故可以忽略这一步 ,直接加上另外一项和p的乘积。
例如(1+x^k)*(1+x^i)=(1+x^k)+(x^i+x^(i+k)) 如果k,i或者k+i任何一项超过了目的指数n,就可以舍弃。因为在指数都是正数的情况下,一旦超越了n不可能再变小的。在两个二项式相乘的过程中,每个r[j] (0<=j<=n)都要和a[i]相乘,故时间复杂度是O(n),对于m个二项式来言,整体复杂度就是O(m*n).
好了原理就解释到这里,下面给出代码:
新的函数代码量降低了近一半(除去注释),更简洁,更优雅,更高效。
使用0至n-1共n个数据作为一个测试项,测试数据测试如下(由于数据量问题,仅测试运行时间,对于数据溢出没有作处理):
可见时间复杂度从O(sigma(1,i,n),C(n,i))提升到O(m*n)的效果是极其显著的,。
结论:
1、时间复杂度的下降是提高运行效率的最有效手段
2、数学方法非常重要,尤其对于这种数值处理的问题。
3、PKU和TSU的同学果然都是无比强大,再次感谢Martix67和腌菜同学,并致以崇高的敬意!
同时欢迎各位同学前来批评指正,只有讨论的越深入,我们才越能了解事物的本质。