出场人物介绍
小美:小学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