算法进化历程3相亲数对

作者: 巧若拙 | 来源:发表于2020-04-12 13:02 被阅读0次

    出场人物介绍

    小美:小学4年级学生,参加了学校的编程兴趣小组,已经了解了Python语言的基本语法,能够看懂一些简单的程序。她做事风风火火,对所有的事情都很好奇,喜欢打破砂锅问到底,是一个叫人又爱又恨的小丫头。

    阿福:一个酷爱编程的8年级男生。大家都说他长得像国宝大熊猫,动作缓慢,憨态可掬。他做事情确实够慢的,连说话也慢条斯理,可是他一点也不担心,他常常说:“慢就是快,只要坚持下去,蜗牛也能爬上金字塔。”

    古老师:虽然年近不惑,但依然对生活充满热情。“爱生活爱运动”是他的人生信条,和孩子们一起编程是他最大的乐趣。他神出鬼没,总是在孩子们最需要帮助的时候出现。当然,你也不能动不动就找古老师,因为他很忙,非常非常忙。所以,遇到问题还是自己先思考吧。


    算法进化历程之相亲数对

    小美:上次古老师教给我们利用数学规律解决灯泡开关问题的方法真是巧妙,尤其是逆向思维求完全平方数个数的方法令人印象深刻,至今仍在我的脑海中回荡呢。

    阿福:有这么夸张吗?既然你印象深刻,那我这里有一道题目你一定会做。


    题目1:

    相亲数对。2500年前数学大师毕达哥拉斯发现,220与284两数之间存在下列奇妙的联系:

    220的真因数之和为1+2+4+5+10+11+20+22+44+55+110=284

    284的真因数之和为1+2+4+71+142=220

    毕达哥拉斯把这样的数对a,b称为相亲数:a的真因数(小于本身的因数)之和为b,而b的真因数之和为a。

    函数功能:输出[2,n]范围内的所有相亲数对。

    函数名:amicable_pair(n:int)-> list

    参数表:n-- 正整数上限。

    返回值:一个存储了所有相亲数对的列表,列表的每个元素都是一个存储了相亲数对的元组。

    示例:n=2000,则返回[(220, 284), (1184, 1210)]。


    小美:这有什么难的,只要用变量i遍历[2,n]范围内的所有整数,然后把i的真因数和存储到变量s,再看看s的真因数和是不是等于i就行了。为了体现模块化编程的思想,我们可以把计算真因数和的功能抽象成一个函数。


    代码1:

    def amicable_pair(n:int) -> list:

       def get_sum(num): #计算num的真因数和

           s = 1

           for i in range(2, num):

                if num % i == 0:

                    s += i

           return s

       res = []

       for i in range(2, n+1):

           s = get_sum(i)

           if i < s and i == get_sum(s):#确保i<s,以避免重复计算数对

                res.append((i, s))

       return res

    阿福:小美,怪不得人家都说你是个女汉子,思维总是这样的简单直接。可你求真因数和的算法也太低效了,一点也没有用到因数对称分布的特点啊。

    小美:因数对称分布?哦,还真是这样,瞧我这记性!那我稍微改进一下,你再看看。


    代码2:

    def amicable_pair2(n:int) -> list:

       def get_sum(num): #计算num的真因数和

           sqr = int(num ** 0.5)

           s = 1  

           if num == sqr * sqr:

                s += sqr

           for i in range(2, sqr):

                if num % i == 0:

                    s += i + num // i

           return s

       res = []

       for i in range(2, n+1):

           s = get_sum(i)

           if i < s and i == get_sum(s):#确保i<s,以避免重复计算数对

                res.append((i, s))

       return res

    阿福:这还差不多!

    古老师:不错,充分利用了因数对称分布的特点,还考虑到了完全平方数的平方根只能加1次。小美进步很大啊!

    小美:都是阿福学长指导有方。

    古老师:都是好孩子!但是你们有没有注意到,在for i in range(2, n+1)循环体内,你们分别以i和s作为参数调用了函数get_sum两次。而s的值在整个循环过程中是有可能重复出现的。

    小美:没错,s存储了i的真因数和,是有可能重复出现的。但这也是没有办法的事情啊,不计算get_sum(s),就无法判断i和s是否为相亲数。

    阿福:有办法!可以把每个整数的真因数和都存储起来,这样每个整数都只需调用一次函数get_sum了。

    小美:有道理,那我明白了!


    代码3:

    def amicable_pair3(n:int) -> list:

       def get_sum(num): #计算num的真因数和

           sqr = int(num ** 0.5)

           s = 1 

           if num == sqr * sqr:

                s += sqr

           for i in range(2, sqr):

                if num % i == 0:

                    s += i + num // i

           return s

       res = []

       p = [0] * (n + 1) #用p[i]来存储i的真因数和

       for i in range(2, n+1):

           s = get_sum(i)

           if s <= n: #确保s<=n,以避免下标越界

                p[i] = s

       for i in range(2, n+1):

           if p[p[i]] == i and i < p[i]:#确保i<p[i],以避免重复计算数对

                res.append((i, p[i]))

    return res

    阿福:小美动作好快!我刚想到思路,你就把代码写出来了。

    古老师:小美的方法固然不错,但阿福你就没有别的想法了吗?

    阿福:别的想法?小美的方法效率已经很高了啊。

    古老师:相比算法2,算法3的效率确实有了大幅度提升,但它仍然停留在调用函数get_sum,计算真因数和的思路上。可get_sum函数本身就是算法的瓶颈所在,就像在统计完全平方数的问题中,开根号运算是瓶颈一样。

    小美:开根号运算是瓶颈?对,我们上次就是逆向思维,用乘法运算代替开根号运算的。

    阿福:逆向思维?该怎么逆向思维呢?

    古老师:放弃原有包袱,去除get_sum函数的思维定式,不要老想着一次性把整数i的真因数和求出来,这样思想就解放了。

    小美:不要一次性把整数i的真因数和求出来?难道还可以分开求吗?

    阿福:分开求?还真是可以啊!我明白了!


    代码4:

    def amicable_pair4(n:int) -> list:

        res = []

        p = [1] * (n + 1) #用p[i]来存储i的真因数和

        for j in range(2, n//2+1):

            for i in range(j*2, n+1, j):

                p[i] += j #因为i是j的倍数,故j为i的某个真因数

        for i in range(2, n+1):

            if i < p[i] <= n and p[p[i]] ==i:#确保i<p[i],以避免重复计算数对

                res.append((i, p[i]))

        return res

    小美:遍历j,然后把j加到它的倍数i的p[i]上去,这样可以用加法运算代替求余数运算,简直是神操作啊!阿福你实在是太棒了!

    古老师:孺子可教也!阿福你的确掌握以空间换时间和逆向思维的精髓了。说起以空间换时间的方法,我这里恰好有一个绝佳的例子,可以作为本节课的课后练习。今天就到这,下次再见咯。


    题目2:

    函数功能:找出字符串中第一个只出现一次的字符

    函数名:first_one(s:str)-> str:

    参数表:s -- 由ASCII码组成的字符串。

    返回值:返回第一个只出现一次的字符,如果不存在返回"No"。

    示例1:

    s="asdhfasdf",返回"h";

    示例2:

    s="asdfasdf",返回" No ";


    彩蛋:

    小美:这不就是统计字符串出现的次数吗,和以空间换时间有什么关系呢?

    阿福:这个我知道,好像是利用了桶排序的思想,把每一个字符都看做是一个桶,该字符每出现一次就装到桶里计数。

    小美:总共256个ASCII码字符,那就需要256个桶啊?

    阿福:是的,计数完毕以后再遍历字符串,就能快速找到第一个只出现一次的字符了。

    小美:原来是这样,那我明白了。


    代码5:

    def first_one(s):

        lib = [0] * 256

        for e in s:

            lib[ord(e)] += 1

        for e in s:

            if lib[ord(e)] == 1:

                return e

        else:

            return "No"

    阿福:使用列表作为桶,需要事先规定桶的数量。还可以使用字典来存储,以某个字符和其出现的次数作为键值对,这样可以扩展到Unicode字符。代码如下:


    代码6:

    def first_one2(s):

        lib = {}

        for e in s:

            lib[e] = lib.setdefault(e, 0) + 1

        for e in s:

            if lib[e] == 1:

                return e

        else:

            return "No"

    相关文章

      网友评论

        本文标题:算法进化历程3相亲数对

        本文链接:https://www.haomeiwen.com/subject/nsmvmhtx.html