2.2关于阶乘的一点知识
问题1:求 N!末尾有多少个0。
问题2:N!中二进制表示中最低位1的位置。
首先对于问题1:
对于N!的末尾有多少个0这个问题。要追溯到算术基本定理:
算术基本定理:
任何一个大于1的自然数N,都可以唯一分解成有限个质数的乘积 N=(P_1^a1)*(P_2^a2)......(P_n^an) , 这里P_1<P_2<...<P_n是质数,其诸方幂 ai 是正整数。
以上是百度百科对于该定理的描述。由此我们可以把一个数看做是由有限个素数因子构成的。那么N!也不例外。设N!=M。
M = S*10^n = S*(2*5)*(2*5)*(2*5)...2*5有n队。S尾部是不含0的。所以原问题就转化为寻找N!中有多少对2和5的因子了。
也就是说寻找2和5的个数中哪个最少。取最少的那个(原先描述最少用最小,果断容易引起误解。)。对于这个问题。还有一步可以通过思维化简问题的就是2和5的因子到底哪个更少呢?明显是5的。所以我们要寻找数中5的因子的个数。
问题描述:寻找 N!中5的因子的个数。
法1:从数的角度
很常规的解法,将数除以5判断是否能被5除尽。可以的话就计数。循环执行。
int count; int num = N!; count = 0; while(num) { if(num%5==0) { count ++; } num /= 5; }
观察这个式子会发现类比求二进制中1的个数这个问题很像。对于这个问题,我们可以考虑一下。2的因子的个数其实就是最后一个1后面的0的个数。
那么我们是否可以用所谓的五进制呢?要将一个数转成一个五进制数本身就耗费太多时间了。但是这是个好想法。学会类比,这是很重要的。
同时这个角度有很大的缺陷,N!往往是很大的。所以我们经常是无法表达出来。
法2:从组合数的角度(结构)
组合数:N!= 1*2*3*4*5*...*N。
观察会发现一个很大的数构成一些小数乘积的结构。其中有一些数有一个5的因子,比如5,10。其中有一些数有2个5的因子。比如25,125。其中有一些数字有3个,比如125。那么按常规的。我们先找有多少个至少有1个5的因子的数。也就是说找1~N中有多少个能被5整除的数。
直接想并不容易。而用逆向思维地想。这些数一定是5*n。凡是5的倍数的就一定能被5整除。(废话)
有1个的时候。N取值 [5,10)就是5.而[10,15)则是2个。那么可以发现就是 [N/5] "[ ]"代表取整。
之后我们再数有2个5的因子的数。再数有3个的。4个的。直到没有。也就是[N/pow(5,k)]==0。
并且根据指数爆炸。pow(5,k)增长是十分快的。算法时间只需要log级别。而对于pow(5,k)我们可以考虑使用快速幂。
另外这种计数原理实质上是容斥定理。关于容斥定理。摘自百度百科。
容斥定理:
在计数时,必须注意无一重复,无一遗漏。为了使重叠部分不被重复计算,人们研究出一种新的计数方法,这种方法的基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。
int count; count = 0; void Count(int N) { temp = (N>>i); while(temp) { count += temp; temp >>= 1; } }
反思:我们要学会类比。逆向思维的思考。(其实这个可以有利于DP的状态转移方程的思考)
然后对于问题2:
求N!的最低位的1的位置。最低位的1的位置不过就是1后面0的个数。那么对于0的个数问题其实可以转化为该数含有多少个2的因子的问题。通过问题1中所述的类比。很简单就可知。所以问题转化为求N!的2的因子的个数问题。这个问题完全可以用上述的方法2来处理。而编程之美上又给出了一个规律。 即1的位置 = N - N中1的个数。
推理过程:
一个二进制的数101001。
根据求法1,可以获得 10100+1010+101+10+1
= 11111+111+1
= 100000-1+1000-1+1-1
= 10101-3.
观察这个等式 10100+1010+101+10+1= 11111+111+1 对于11111 是由101001这个数最左边的1向右边移动而获得的。
类比的。111是由101001由左往的1 构成。而1就是由101001最右边的1构成的。
而 11111正好是比原来的100000少1 所以就可以简单发现这个规律的恒成立。其中包含了。二进制的一些性质在内。
这个规律明显是由法1得来的所以只对阶乘数有效。
在这里就不附上问题2的代码了。