蓝桥杯刷题014——求阶乘(二分法)

求阶乘 

蓝桥杯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+=1
    m//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 //= 5
        res += N
    return res

复杂度:每一次变成原来的五分之一,所以复杂度为O(log_5N),是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 //= 5
        res += N
    return res

N = 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)//2
        if cal_zero(mid)>=k:    
            R = mid
        else:
            L=mid
    if cal_zero(R)==k:  # cal_zero(r):满足阶乘末尾0≥k的最小整数
      return R
    else:               # 没有阶乘末尾k个0的整数
      return -1   

 二分法的复杂度也是O(logN),所以算法复杂度为O((logn)^2)

代码演示: 

k=int(input())
def cal_zero(N):
    res = 0   # 统计末尾0的个数
    while N:
        N //= 5
        res += N
    return res
def check(k):
    L,R=1,int(1e19)    
    while L+1!=R:
        mid = (L+R)//2
        if cal_zero(mid)>=k:    
            R = mid
        else:
            L=mid
    if cal_zero(R)==k:  # cal_zero(r):满足阶乘末尾0≥k的最小整数
      return R
    else:               # 没有阶乘末尾k个0的整数
      return -1   
print(check(k))

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小叶pyか

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值