6.6 完数(project) educoder项目实训

文章讲述了作者在解决Python编程作业时,从基础练习题逐渐深入到数论问题,如完全数的求解、素性检验方法的优化,以及欧拉筛法的应用,体现了数学在算法中的重要性。
摘要由CSDN通过智能技术生成

前言

        在最近的Python课上,做到这样一个“有趣”的作业,求得前n个完数,并输出其与其真约数的约数的加和式子,刚开始没怎么在意这个题,想着不就是做过好几遍了的语言基础练习题嘛,但是接下来的几小时没想到都在肝这个,具体原因请继续往下看

第一关

到现在为止一切安好,就是基础语言练习题而已,秒了

代码

import math

def f(n):
    r = 1
    for i in range(2, int(math.sqrt(n))+1):
        if(n % i == 0):
            r += (i + n / i)
    return r == n

cnt,num = 0,2
x = int(input())
while cnt < x:
    num += 1
    if f(num):
        fac = [i for i in range(1,num) if num%i == 0]
        s = str(num) + '='
        for i in fac:
            s += str(i) + '+'
        print(s.rstrip('+'))
        cnt += 1

第二关

        我一看题目完全没变,寻思怎么能出一样的题目呢??然后直接ctrl c + ctrl v开始评测,正当我沉浸在题目秒了的喜悦中,我发现了这样一个东西

然后不出所料,没能在20s内完成任务,TLE了

于是我开始在网上搜寻有关完全数的数学知识,很快就找到了欧拉提出的完全数获得公式:

如果p是质数,且2^p-1也是质数,那么(2^p-1)* 2^(p-1)便是一个完全数。

用这样的办法改进一下代码还是很easy的,不久就得到了这样一个崭新出厂的代码

代码

import math

def is_prime(n):
    for i in range(2, int(math.sqrt(n))+1):
        if(n % i == 0):
            return False
    return True


cnt, num = 0, 2
x = int(input())
while cnt < x:
    if is_prime(num) and is_prime(2 ** num - 1):
        a = 2 ** (num-1) * (2 ** num - 1)
        s = str(a) + '='
        fac = [1]
        for i in range(2,2 ** num - 1):
            if(a % i == 0):
                fac.append(i)
                fac.append(a // i)
        fac.sort()
        print(s + str(fac).replace(', ','+').lstrip('[').rstrip(']'))
        cnt += 1
    num += 1

ac题目总是让人心情愉悦的,所以我顺势点开了下一关

 第三关

这回我学聪明了,先去看了看测试集,虽然有心理预警,但仍然吓了一跳

阶段1

当我看到这个9心里已经凉了一半了,另一半是当我cv提交测试没过的时候彻底凉透的,那么很显然需要一些数学上的优化,此时我想到如果单独将素数罗列出来,可以节省一次素性检验,只需检验 (2 ^ n) -1 是否也为素数即可,但是这样节约的时间仍然不足以让题目完全ac。

阶段2

此时我发现我做了太多的除法和模运算,对于大数的计算是很费时间的,于是我开始观察约数的序列,得到这样一个事实:除了 (2^n)-1 , 完数a的其他约数都是 2 ^ n的形式,于是我想到可以将他们特殊处理,这里把1也放进来是为了防止完数a本身也进入列表,此时已经可以ac题目了。

阶段3

但是150s的运行时间让我觉得下一关肯定也过不了,不如所性继续优化,于是我想起一种更快速的,类似于筛法的素性检验算法,更换了素性检验算法后时间降至50s左右,此时我再一次沉浸在喜悦之中

代码

import math


def is_prime(n):
    if n <= 1:
        return False
    if n <= 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False
    i = 5
    while i * i <= n:
        if n % i == 0 or n % (i + 2) == 0:
            return False
        i += 6
    return True


cnt = 0
prime = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61]
index = 0
x = int(input())
while cnt < x:
    # 欧拉的完数公式 : 如果 n 和  (2**n -1) 同为素数 则 (2^(n-1)) * ((2^n) -1)为完数
    if is_prime(2 ** prime[index] - 1):
        a = 2 ** (prime[index]-1) * (2 ** prime[index] - 1)
        s = str(a) + '='
        # 寻找约数
        fac = [1]
        for i in range(1, prime[index]):
            t = 2 ** i
            fac.append(t)
            fac.append(a // t)
        fac.sort()
        print(s + str(fac).replace(', ', '+').lstrip('[').rstrip(']'))
        cnt += 1
    index += 1

第四关

        此时内心想法其实是:第三关运行时间给了200s,第四关应该给的更多吧。但点进去一看,居然只有20s??!!而且这个测试集我&*%……&%¥#%

压抑住爆粗口的冲动,接着想优化的方法。

我先想着 2^(n)-1这样的数有没有什么特殊性质,然后发现这个东西叫做梅森数,而且发现,对于梅森数有着一种特殊的素性检验法,叫做lucas-lehmer检验法​​​​​​,这真是解决了大问题,然后我又发现,我所需要的素数序列,靠人力最多写到一百多,就要花很多时间在手动计算素数上了,这很不符合程序设计的理念(我要是自己算要程序干嘛!!!)那么曾经学过一种得到素数序列的犯法叫做欧拉筛法(也称线性筛法)就派上用场了,O(n)的时间复杂度太香了!!基本不会挤占我的运行时间。除此之外,我又将乘除法和阶乘等运算换成了对应的位运算,更尽可能的加速程序的运行,功夫不负有心人,终于解决了这样的问题,并且运行时间不到1s!!!(跨越性进步好耶!)

代码

import math


# 欧拉筛法获取小于r的素数列表
# 可以节省对每个n进行素性检验的过程,避免更多的计算
def oula(r):
    # 全部初始化为0
    prime = [0 for i in range(r+1)]
    # 存放素数
    common = []
    for i in range(2, r+1):
        if prime[i] == 0:
            common.append(i)
        for j in common:
            if i*j > r:
                break
            prime[i*j] = 1
            #将重复筛选剔除
            if i % j == 0:
                break
    return common


'''
卢卡斯-莱默素性检验法
令梅森数 M[p]=2^p-1作为检验对象,
定义数列 L[n]:L[n-1] ^ 2 - 2,n>0. 
这个数列的开始几项是4,14,194,37634,1416317954……
那么M[p]是质数当且仅当L[p-2] ≡ 0 (mod M[p])
否则Mp是合数。sp−2模Mp的余数叫做Mp的卢卡斯-莱默余数。
'''
def primality(N, M):
    if N == 2:
        return True
    s = 4
    for i in range(N-2):
        s = (s * s - 2) % M
    return s == 0


cnt,index = 0,0
prime = oula(2000)
# 可以刚好得到完数的 n,虽然可以节省一定时间,但通过答案倒推过程不符合程序设计的合理性
# res = [2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279]
x = int(input())
while cnt < x:
    # 欧拉的完数公式 : 如果 n 和  (2**n -1) 同为素数 则 (2^(n-1)) * ((2^n) -1)为完数
    m = (1 << prime[index])- 1
    if primality(prime[index],m):
        a = m << (prime[index]- 1)
        s = str(a) + '='
        '''
        寻找约数
        观察数据不难得知,除了m,a其余的约数都是 2 ^ n 形式,
        这里初始化将1也放进来是避免 a 本身进入列表中
        那么我们通过这个结论,可以不再使用试除法获取约数列表
        节省更多的时间
        '''
        fac = [1,m,(m+1)>>1]
        num1 = 2
        num2 = a >> 1
        '''
        误区记录:insert具有O(n)的时间复杂度,相比append的O(1),
        虽然节省了排序的O(nlogn),但数据大时并不更省时。
        for i in range(1, factors[index]-1):
            fac.insert(i+2,num2)
            fac.insert(i,num1)
            num1 <<= 1
            num2 >>= 1
        '''
        for i in range(1, prime[index]-1):
            fac.append(num1)
            fac.append(num2)
            num1 <<= 1
            num2 >>= 1
        fac.sort()
        print(s + str(fac).replace(', ', '+').lstrip('[').rstrip(']'))
        cnt += 1
    index += 1

总结

        一个小小完数,居然涉及如此广泛的数论知识,深刻提醒我数学在计算机领域的运用确实是十分广泛的,学习算法的路上,数学一定是一大助力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

.芊羽.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值