算法进化历程4丑陋数

出场人物介绍

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

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

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


算法进化历程之丑陋数

小美:上次古老师教给我们以空间换时间的方法真是巧妙,用它来解题可以提高不少效率呢。

阿福:是的,以空间换时间换时间是一种常用的技巧,在很多地方都有用到。今天这里有一道数学题目,你看看会不会做?


题目1:

丑陋数。丑陋数是指质因数只包含2,3,5的自然数,例如前十个丑陋数依次为:1, 2, 3, 4, 5,

6, 8, 9, 10, 12。

给定一个自然数n (n <= 1500),请你求出对应的第n个丑陋数。

函数功能:丑陋数是指质因数只包含2,3,5的自然数,输出第n个丑陋数。

函数名:ugly_number(n:int)-> int

参数表:n-- 一个自然数。

返回值:第n个丑陋数。

示例1:n=2,则返回2;

示例2:n=7,则返回8;

示例3:n=11,则返回15。


小美:丑陋数?这名字不好听,不过题目倒不难,我用最简单的枚举算法就能解决它。


代码1:

def ugly_number(n:int) -> int:

    s = 1

    i = 2

    while s < n:

        num = i

        for e in [2, 3, 5]:

            while num % e == 0:

                num //= e

        if num == 1: #i是丑陋数

            s += 1

        i += 1

    return i - 1 #注意当s==n时,i还自增了一次 

阿福:小美,别老是用这么简单粗暴的方法好不好,以时间换空间只是挂在嘴边的一句话而已吗?

小美:哦,不好意思,心又急了。这里确实好像可以先建一个丑陋数表,因为每个丑陋数都是由比它小的丑陋数乘以2或3或5得到,可以利用已经求得的丑陋数表来判断一个数是否为丑陋数。


代码2:

def ugly_number_2(n:int) -> int:

   lib = [1] #丑陋数表

   s = 1

   i = 2

   while s < n:

       if ((i % 2 == 0 and i // 2 inlib) or

            (i % 3 == 0 and i // 3 in lib) or

            (i % 5 == 0 and i // 5 in lib)):

               s += 1

               lib.append(i)

       i += 1

   return i - 1 #或lib[n-1]

阿福:这还差不多,但是判断某个数是否在丑陋数列表中的效率并不高,能不能改用其他的数据结构来存储丑陋数表?

小美:在列表中查找元素默认是用顺序查找算法,效率确实不高。但是改用什么数据结构好呢?

阿福:查找速度快的有哪些数据结构?

小美:集合和字典都很快。对了,我可以用集合来存储丑陋数表。


代码3:

def ugly_number_3(n:int) -> int:

    lib = {1} #丑陋数表

    s = 1

    i = 2

    while s < n:

        if ((i % 2 == 0 and i // 2 in lib) or

            (i % 3 == 0 and i // 3 in lib) or

            (i % 5 == 0 and i // 5 in lib)):

               s += 1

               lib.add(i)

        i += 1

return i – 1

古老师:挺好!阿福指导有方,小美一点就通,都挺好!

阿福:老师过奖了,这也不是什么难题。

古老师:题目虽然不难,但是你们追求算法效率的精神是难能可贵的,值得表扬!其实除了使用集合存储数据来突破算法2的瓶颈,我们还有更好的方法,那就是根本不去判断某个数是否已经存在于丑陋数列表中。

小美:不使用成员运算符in?

古老师:没错。我问你,为什么要符判断某个数是否在丑陋数列表中?

小美:为了避免将相同的整数重复增加到列表中。

古老师:回答正确!如果我们能保证增加到列表中的元素值是递增的,是否还需要作此判断?

小美:那就不需要了。但是怎样做才能保证增加到列表中的元素值是递增的呢?

阿福:可以在列表元素值的2,3,5倍值中取最小值增加到列表中。

古老师:好想法!那具体该怎么做呢?

阿福:我们用列表lib来存储丑陋数,设lib[0]=1,然后设置3个指针变量a,b,c,分别用来计算某几个元素的2,3,5倍值,然后从中取最小值增加到列表中。若最小值是lib[a] * 2,则令a增一;若最小值是lib[b] * 3,则令b增一;若最小值是lib[c] * 5,则令c增一。这样既可以确保每次增加到列表中的值是当前最小丑陋数,又无需遍历列表来判断某个数是否已经存在于丑陋数列表中。

古老师:没错,这种记录各个子问题的解,并利用子问题的解来解决总问题的方法就是传说中的动态规划。阿福,既然你已经有思路了,就把代码写出来吧。

阿福:好的!


代码4:

def ugly_number_4(n:int) -> int:

    lib = [1] #丑陋数表

    a, b, c = 0, 0, 0#a,b,c分别表示某元素即将乘以其2,3,5倍

    for i in range(n):

        lib.append(min(lib[a]*2, lib[b]*3,lib[c]*5))

        if lib[-1] == lib[a] * 2:

            a += 1

        if lib[-1] == lib[b] * 3:

            b += 1

        if lib[-1] == lib[c] * 5:

            c += 1

return lib[n-1]

小美:动态规划,其实代码也挺简洁呢,没想到效率这么高!

古老师:没错,动态规划也是以空间换时间的简单策略,今后我们还要经常使用它来高效解决一些最优解问题。今天就到这,布置一道练习题,下次再见咯。


题目2:

学校为1000名新生每人分配了一个序号,序号从1开始递增到1000。

小明的序号是2,他想设置一个微信群,把序号是自己的3倍和8倍的同学(即6号和16号)拉进群里。之后每个群友都可以把序号是自己的3倍和8倍的同学拉进群里。根据题目描述,编写定义函数判断序号为x的同学能否加入微信群。

描述:判断序号为x的同学能否加入微信群

函数名:defteammate(e:int, x:int)->bool

参数表:e -- 已知序号为e的同学可以加入微信群;

        x --判断序号为x的同学能否加入微信群。

返回值:若序号为x的同学可以入群返回True,否则返回False。

示例:当e=2,x=18时,返回True;当e=2,x=24时,返回False。




彩蛋:

小美:这个题目看起来和求丑陋数很相似,但我觉得最简单的写法是递归。

阿福:是吗?那写出来瞧瞧。


代码5:

def teammate(e:int, x:int)->bool: #使用递归算法求解

    if e > x:

        return False

    elif e == x:

        return True

    else:

        return teammate(3*e, x) orteammate(8*e, x)

阿福:确实很不错!递归算法的代码简明易懂,但也有效率低下的缺点,我们通常用迭代的方法来代替递归,以提高效率。你能把代码5改成迭代的形式吗?

小美:迭代算法?好像有点难,让我试试看。


代码6:

def teammate6(e:int, x:int)->bool: 

   while x > e:

       if x % 3 == 0:

           x //= 3

       elif x % 8 == 0:

           x //= 8

       else:

           break

return x == e

阿福:漂亮!迭代和递归的思考方向刚好相反,迭代是自底向上,递归是自顶向下。你的代码充分体现了迭代算法的特征,效率很高。除了使用迭代,我们还可以用递推的方法来代替递归算法。你能把代码5改成递推的形式吗?

小美:递推?那首先要知道递推式才行,可这道题目的递推式太难了,我写不出来。

阿福:没错,有些递推式表达起来确实比较麻烦。给你点提示,求丑陋数表的算法4其实采用的就是递推方法,你可以模仿它。

小美:那倒是,有可以模仿的对象就好办了。


代码7:

def teammate7(e:int, x:int)->bool: 

    s= [e]

    a= b = 0

    whiles[-1] < x:

       s.append(min(s[a]*3, s[b]*8))

       if s[-1] == s[a] * 3:

           a += 1

       if s[-1] == s[b] * 8:

           b += 1

return s[-1] ==x


阿福:不错,一点就通!其实除了双指针,这道题目也可以使用队列来解决,我把代码写给你看一下。


代码8:

def teammate8(e:int,x:int)->bool: 

    s = [e]

    head, rear = 0, 1

    while head < rear:

        if s[head] * 3 <= x and s[head] * 3not in s:

            s.append(s[head] * 3)

            rear += 1

        if s[head] * 8 <= x and s[head] * 8not in s:

            s.append(s[head] * 8)

            rear += 1

        head += 1


return x in s


补充:除了上述解法,网友“毛毛”还提供了很漂亮的枚举算法,代码如下:


代码9:

def teammate9(e:int, x:int)->bool: #使用枚举算法求解

    for i in range(int(math.log(x))+2) :

        for j in range(int(math.log10(x))+2) :

            if (3**i)*(8**j)*e == x:

                return True

    return False

你可能感兴趣的:(算法进化历程4丑陋数)