出场人物介绍
小美:小学4年级学生,参加了学校的编程兴趣小组,已经了解了Python语言的基本语法,能够看懂一些简单的程序。她做事风风火火,对所有的事情都很好奇,喜欢打破砂锅问到底,是一个叫人又爱又恨的小丫头。
阿福:一个酷爱编程的8年级男生。大家都说他长得像国宝大熊猫,动作缓慢,憨态可掬。他做事情确实够慢的,连说话也慢条斯理,可是他一点也不担心,他常常说:“慢就是快,只要坚持下去,蜗牛也能爬上金字塔。”
古老师:虽然年近不惑,但依然对生活充满热情。“爱生活爱运动”是他的人生信条,和孩子们一起编程是他最大的乐趣。他神出鬼没,总是在孩子们最需要帮助的时候出现。当然,你也不能动不动就找古老师,因为他很忙,非常非常忙。所以,遇到问题还是自己先思考吧。
算法进化历程之灯泡开关
阿福:最近古老师让我到力扣(leetcode)网去刷题,我遇到了一个很有趣的题目。
小美:是吗?赶快拿出来瞧瞧。
题目1:
灯泡开关。
n个灯排成一排,依次编号为1-n,开始时都是关着的。现进行如下操作: 第 1 轮,所有电灯的按钮按动一次;第2 轮,所有编号为2的倍数的电灯按钮按动一次;第3轮,所有编号为3的倍数的电灯的按钮按动一次;……第n 轮,所有编号为n的倍数的电灯的按钮按动一次。最后请统计 n 轮后有多少只电灯是亮的。
函数功能:统计 n 轮后有多少只电灯是亮的。
函数名:bulbSwitch(n:int)-> int
参数表:n-- 灯的数量。
返回值: n 轮后亮着的灯的数量。
示例1:n=3,则返回1。
解释:初始时,灯泡状态 [关闭, 关闭, 关闭];第一轮后, 灯泡状态 [开启, 开启, 开启];
第二轮后,灯泡状态 [开启, 关闭, 开启];第三轮后, 灯泡状态 [开启, 关闭, 关闭]。
故第3轮后,只有编号为1的灯泡是亮的。
示例2:n=10,则返回3。
解释:第10轮后,编号为1,4,9的灯泡是亮的。
小美:我感觉这道题不难啊,用模拟算法就能解决。我们可以分别用0和1表示灯泡的暗亮状态,然后模拟整个开关灯过程,最后统计值为1的元素个数。
代码1:
def bulbSwitch(n: int) -> int:
a = [0] * (n + 1) #初始时所有灯泡均关闭
for i in range(1, n + 1):
for j in range(i, n + 1, i): #只处理i的倍数
a[j] = 1 - a[j] #实现0和1的相互转换
return sum(a)#对列表a求和,即统计值为1的元素个数
阿福:这个思路简单直接。我一开始也是这样想的,可是提交代码以后,系统判定超时。
小美:超时?那说明我们的算法效率太低了。可是我已经尽可能对代码进行优化了啊。
阿福:没错,如果使用模拟算法的话,你的代码已经写得很好了。可是模拟算法本身就不是高效的算法。这道题目要用到一些数学知识。
小美:数学知识?
阿福:没错,就是数学知识。请你仔细观察一下,n轮操作下来,编号为i的开关切换的次数与i有什么关系?例如当n=10时,编号为6的开关总共切换了几次?分别是在第几轮被切换过?
小美:当n=10时,编号为6的开关在第1轮、第2轮、第3轮和第6轮被切换过,总共切换了4次。
阿福:编号为9的开关呢?
小美:当n=10时,编号为9的开关在第1轮、第3轮和第9轮被切换过,总共切换了3次。
阿福:看出规律来了吗?
小美:规律?哦,我看出来了。编号为i的开关切换的次数恰好为i的因数个数。
阿福:没错,正是这样!我们可以根据编号i的因数个数来判断灯的暗亮状态,若i的因数个数为奇数,则最后该灯亮。
小美:道理是没错,但怎么判断i的因数个数是奇数还是偶数呢?
阿福:你再观察一下6和9的因数分布情况。
小美:我发现了,因数都是对称分布的。例如,6=1*6=2*3,9=1*9=3*3。i的因数以平方根为轴对称分布,成对出现。如果i是完全平方数,它的因数有奇数个(因为平方根只能和自己配对),否则就是偶数个。
阿福:没错,n轮操作下来,只有编号为完全平方数的灯泡亮,我们就只需统计有多少个完全平方数就行了。代码如下:
代码2:
def bulbSwitch2(n: int) -> int:
s = 0
for i in range(1, n + 1):
sqr = int(i ** 0.5)
if i == sqr * sqr: #统计完全平方数的个数
s += 1
return s
小美:阿福真棒!
阿福:唉,当时我也觉得自己挺棒的。可代码提交以后,系统仍判定超时。
小美:这都不行啊?难道还有什么更高效的方法吗?会不会是因为开根号的操作太耗时了?
阿福:我也猜测是这个原因。可不开根号还能怎么办呢?
古老师:阿福,你采用的是枚举算法,那你还记得枚举算法要考虑的因素是哪几个吗?
小美:这个我知道!枚举算法要考虑的因素有枚举变量,枚举范围和枚举条件。
古老师:不错,那在这个题目中,它们分别是什么呢?
阿福:枚举变量是i,枚举范围是[1, n],枚举条件是判断i是否为完全平方数,通过计算i的平方根sqr来判断枚举条件是否成立。
古老师:是啊,这个算法中最耗时的地方就是计算sqr的值。那你们有没有想过逆向思维,枚举sqr,利用sqr来求i呢?
小美:逆向思维?枚举sqr?哦,我想到了!可以从1开始递增sqr的值,判断sqr *
sqr是否小于等于 n,最后sqr的值就是完全平方数的个数。
阿福:不对, sqr的值减一才是正解,因为最后一个sqr已经不满足条件了。
小美:没错,是要减一。还是阿福考虑周到。
代码3:
def bulbSwitch3(n:int) -> int:
sqr = 1
while sqr * sqr <= n:
sqr += 1
return sqr - 1 #最后一个sqr已经不满足条件了
阿福:哇,通过了!还战胜了62.72%的用户呢。
古老师:不错不错!看来你对完全平方数已经有一定的了解了。
小美:一定的了解?难道我们研究的还不够透彻吗?
古老师:应该说研究得比较透彻了。但请你再仔细观察一下,[1, n]范围内的完全平方数个数与n到底有什么关系呢?
小美:这还有关系?
阿福:我把n和[1, n]范围内的完全平方数个数s组成一个元组(n, s),列举了[1, 20]范围内的(n, s)值如下,(1, 1),(2, 1),(3, 1),(4, 2),(5, 2),(6, 2),(7, 2),(8, 2),(9, 3),(10, 3),(11, 3),(12, 3),(13, 3),(14, 3),(15,3),(16, 4),(17, 4),(18, 4),(19, 4),(20, 4)。小美,你发现有什么规律没有?
小美:规律?我看到了,只有当n等于完全平方数的时候,s的值才会增加。
阿福:没错!而且s的值恰好等于n的平方根。
小美:那就好办了!代码简单得很!
代码4:
def bulbSwitch4(n:int) -> int:
return int(n ** 0.5) #[1, n]范围内的完全平方数个数恰好为其平方根
古老师:果然是高手啊,观察力一流!看你们这么厉害,我就再给你们出一道题目吧。题目不难,就是把上一题稍微改动一下。
题目2:
哪些电灯是亮的。
n个灯排成一排,依次编号为1-n,开始时都是关着的。现进行如下操作: 第 1 轮,所有电灯的按钮按动一次;第2 轮,所有编号为2的倍数的电灯按钮按动一次;第3轮,所有编号为3的倍数的电灯的按钮按动一次;……第n 轮,所有编号为n的倍数的电灯的按钮按动一次。最后请输出 n 轮后有哪些电灯是亮的。
函数功能:输出 n 轮后有哪些电灯是亮的。
函数名:bulbSwitch(n:int)-> list
参数表:n-- 灯的数量。
返回值:一个存储了所有亮灯编号的列表。
示例1:n=10,则返回[1, 4, 9]。
示例2:n=30,则返回[1, 4, 9, 16, 25]。
小美:好像没什么区别嘛。
阿福:区别还是有的,需要输出具体哪些电灯是亮的。这样一来就不能直接套用数学公式了。
古老师:虽然不能直接套用数学公式,但是前面总结的数学规律还是有用的。也别想得太复杂,其实只要把前面的代码1、2、3修改一下就行了。好了,我只能说这么多了,剩下的自己去琢磨吧。拜拜咯。
彩蛋:
小美:只要把代码1、2、3修改一下就行了?
阿福:没错,前面我们做的是统计工作,现在只要把每个亮灯的编号都存储起来就行了。这样吧,你修改代码1,我来修改代码2和代码3。
小美:好的,没问题。
代码4:
分别用0和1表示灯泡的暗亮状态,然后模拟整个开关灯过程,最后输出值为1的元素
def bulbSwitch4(n:int) -> list:
a = [0] * (n + 1) #初始时所有灯泡均关闭
for i in range(1, n + 1):
for j in range(i, n + 1, i):
a[j] = 1 - a[j]
return [i for i in range(n + 1) if a[i] == 1]
代码5:
编号为完全平方数的灯泡亮,枚举i,计算sqr
def bulbSwitch5(n:int) -> list:
res = []
for i in range(1, n + 1):
sqr = int(i ** 0.5)
if i == sqr * sqr:
res.append(i)
return res
代码6:
编号为完全平方数的灯泡亮,枚举sqr,存储sqr*sqr
def bulbSwitch6(n:int) -> list:
res = []
sqr = 1
while sqr * sqr <= n:
res.append(sqr * sqr)
sqr += 1
return res