求阶乘
蓝桥杯2022省赛题目
问题描述
满足 N ! 的末尾恰好有 K 个 0 的最小的 N 是多少?
如果这样的 N 不存在输出 −1 。
输入格式
一个整数 K 。
输出格式
一个整数代表答案。
样例输入
2
样例输出
10
评测用例规模与约定
对于 30% 的数据, 1≤K≤10^6.
对于 100% 的数据, 1≤K≤10^18.
思路:
题目大意:求满足N!的末尾恰好有K个0的最小的N,如果这样的N不存在,返回-1
解法一:暴力法
遍历1~10^18(题目中100%的数据规模)内所有数,对每个数求阶乘,再计算末尾0的个数,最后判断是否为K个0,很明显是超时了(看下面代码分析)。但可以得到部分的分数,没有时间的话可以这样简单处理。
代码分析:这个是计算末尾0的个数的代码,很明显在这个环节就没办法达到100%数据的要求,因为1≤K≤10^18,而这个代码复杂度是O(n),要计算10^18次,但蓝桥杯计算量一般不超过10^8,所以用暴力法计算是超时的。
res = 0 # 统计末尾0的个数
while m % 10 == 0:res+=1m//10 # 去掉最后一位
解法二:
解题关键:给出一个N,如何快速计算它的阶乘中末尾0的个数?
- 思考1:什么样的数,相乘后能够产生0?
10=2*5
20=2*2*5
7200=72*2*5*2*5
我们可以发现每个数字末尾的每个0都可以看成是2和5相乘得到
所以我们可以对题目中样例进行分析:10!=1 * 2 * 3 * 4 * 5* 6 * 7 * 8 * 9 * 10,我们发现10!内有一对现成的2和5,但样例输出是2,说明还有一对2和5,没错,只要把10进行分解成2*5就可以再得到一对,这样两对2和5说明10的阶乘末尾有2个零。
结论:给定一个数的阶乘,计算它的因子中2*5出现的次数,即可确定末尾0的个数
- 思考:2:找2*5的数目,因子2是否需要寻找?
不需要。通过10!=1 * 2 * 3 * 4 * 5* 6 * 7 * 8 * 9 * 10可以发现,因子5只有在5和10中才有。但因子2在2,4,6,8,10中都存在,出现2多5少的现象,所以只需要找到稀有的因子5即可,因为因子2是肯定有剩余的。
问题转换:
求N的阶乘尾部0的个数 求N的阶乘中因子5的个数
对阶乘中的因子5进行分析,1 2 3 4 5…10 …15…20…25…30…35… 50…55… 75…100…105…125…,可以发现当到24时,前面每隔5都会都会有一对2和5,共有4对。但在25时会出两个5(两对2和5),总共就有6对,如果你输入5的话,没有末尾为5个0的阶乘,则返回-1。在124之前每隔25会出两对,但125会出三对。把前面出现的5加起来总共有31个,但这样很麻烦,有没有更容易操作的方法呢?看看下面的操作:
为什么是这样算呢?是因为我们先把含有一个及以上5的25个数全部取出一个5加到总数num,那么本来一个5的数就变成0个(可以忽略),本来两个5的数变成一个5的数,本来3个5的变成二个5的。再这样对原本含有二个及以上5的5个数(现在是含有一个及以上5的数)操作一次,只剩下一个含5的数,最后再对含5的1个数取出一个5加到总数num,这样就把全部的因子5转移到了总数num。
结论:求N的阶乘中因子5的个数,将N每次除以5的商求和即可。
定义求一个整数阶乘末尾0的个数的函数:
def cal_zero(N):res = 0 # 统计0的个数while N:N //= 5res += Nreturn res
复杂度:每一次变成原来的五分之一,所以复杂度为,是logN级别的,计算10^18的数只需要算26次即可,非常高效!
注意:题目中 1≤K≤10^18的K是指整数阶乘末尾0的个数的范围,不是整数的范围,说明整数范围可能更大,我们以10^19的整数试一下,看看阶乘末尾0的个数能不能大于10^18。
def cal_zero(N):res = 0 # 统计0的个数while N:N //= 5res += Nreturn resN = 1e19
print(cal_zero(N)) # 2.5e+18
很显然,10^19的整数阶乘末尾有2.5e+18个0,大于题目100%数据大小,是满足要求的。所以N可以取10^19来计算。
上面只是求出了整数阶乘末尾0的个数,还需要找出满足末尾K个0的最小整数,下面用二分法来求解。
二分法登场啦!
使用条件:更小的N对应的是小的尾0个数,更大的N对应的是大的0个数,所以末尾0的个数是一个递增的有序数列,可以用二分法来求解。
定义一个check()函数:将所有从1到N的阶乘分成尾部0个数<k的左半部分和>=k的右半部分,二分结束后检查R位置(因为R是满足≥check()的最小值)的尾部0个数是否为k即可,若不是即返回-1。
def check(k):L,R=1,int(1e19) while L+1!=R:mid = (L+R)//2if cal_zero(mid)>=k: R = midelse:L=midif cal_zero(R)==k: # cal_zero(r):满足阶乘末尾0≥k的最小整数return Relse: # 没有阶乘末尾k个0的整数return -1
二分法的复杂度也是O(logN),所以算法复杂度为
代码演示:
k=int(input())
def cal_zero(N):res = 0 # 统计末尾0的个数while N:N //= 5res += Nreturn res
def check(k):L,R=1,int(1e19) while L+1!=R:mid = (L+R)//2if cal_zero(mid)>=k: R = midelse:L=midif cal_zero(R)==k: # cal_zero(r):满足阶乘末尾0≥k的最小整数return Relse: # 没有阶乘末尾k个0的整数return -1
print(check(k))