最近,北大的韦神又一次冲上热搜,大体是说六个博士四个月没有解决的仿真问题,韦神一晚上搞定了。虽然韦神已经出来辟谣,说这个是假消息,但是并不妨碍我们学习韦神对待生活和工作的态度和精神。
下面我们就以一个真题目,来看一下数学是如何简化计算机程序的。
问题:有N个灯放在一排,从1到N依次顺序编号,初始状态都为关闭。有N个人,也从1到N顺序编号。1号将灯全部打开,2号将凡是2的倍数的灯全部关闭;3号将3的倍数的灯全部作相反操作(该灯如为打开,则将它关闭;如关闭,则将它打开)。以后的人,都和3号操作一样,将凡是自己序号倍数的灯作相反操作。第N个人操作完之后,一共有几盏灯亮着?
这个题目虽然相当的二,不过还是有研究的价值的。
那么我们还是先来进行蒙特卡洛模拟(暴力破解)。
使用计算机真实模拟整个过程,当全部模拟完成后,统计一下还有几盏灯亮着就可以了。
具体逻辑如下:
def create_light(num):
l = list()
for i in range(0,num+1):
l.append(False)
return l
def open_close(llst,start):
for i in range(start,len(llst)):
if i % start == 0: # 满足是自己位置倍数的数
llst[i] = not llst[i] # 开关灯
def get_light_open(llst):
cnt = 0
for l in llst:
if l:
cnt += 1
print("result:",cnt)
def baoli(lightlst,count):
for i in range(1,count+1): # 每个人依次出队
open_close(lightlst,i) # 每个人依次开灯关灯
pt = ""
for l in lightlst[1:]:
if l:
pt += ' ON,'
else:
pt += 'OFF,'
print("%02d"%i,pt) # 打印本次结果
get_light_open(lightlst) # 打印最终结果
从上面的这张结果图,我们可以从01一直看到10,并用题目所说的方法验证,可以看出整个过程无问题。检查最后一行后,可以得出10个人10盏灯,操作过后,最终会有3盏灯处于亮的状态。
到这一步,其实跟网上其他的教程都差不多,那么下面,我们就要开始用数学来重新思考这个题目了。本文提供的教程,会比网上其他的教程更深入些,其他教程分两种,要么是通过暴力分析后,通过输出找规律来说明更简便的方法;要么是泛泛地谈了下如何分析,基本没有中间的推理过程。
下面就开始我们的分析。
1. 灯的状态只有两种,亮和灭,所以某一盏灯最终的状态如何,取决于这一盏灯被操作了几次。初始状态灯都是灭的,所以某一盏灯被操作奇数次,则其最终状态为亮;被操作偶数次,其最终状态为灭。
2. 因为每一个人都是从与自己的位置对应的位置开始对灯做操作,因此经过一轮的操作不会对自己位置号之前的灯的状态有任何影响。
3. 假设位置为N的灯,都谁会操作到这盏灯呢?可以被N整除的所有数,都可以操作到这盏灯。也就是说号码是N的约数的人,都可以操作到这盏灯。
根据上面的分析,这个题就变成了一道地地道道的数学题,即N个人和N盏灯,检查从1到N的每一个数的约数的个数,个数为偶数,状态为0(灯灭);个数为奇数,状态为1(灯亮),最终找到1的个数就可以了。
再简化一下题目:给出一个数N,找到1 ~ N中的整数的每一个约数个数为奇数的数及这些数的个数。
OK,关于约数的个数,有公式如下,求一个数的因数个数方法是先把这个数分解成质数幂次相乘的形式,然后把各个质因数的幂次加一再做相乘得到。比如 24 = 2^3 * 3^1,那么24的因数个数就是(3+1)*(1+1)=8个。(本质上是排列组合,这里不做详细论述,感兴趣的可以到知乎上找找)
若N = x1^a * x2^b *.... * xn^z,则因数个数=(a+1)*(b+1)*....*(z+1)
1. 偶数*偶数=偶数,偶数*奇数=偶数,奇数*奇数=奇数
2. 因数个数为奇数,必然a,b,....z,每一个都为偶数
3. x1^a,a=2时,x1^2;a=2n时,x1^2n=(x1^n)^2,所以a为偶数,x1^a必然可以转换为xn^2形式
4. x1^a * x2^b *.... * xn^z = xa^2 * xb^2 *...* xn^2 = (xa*xb*...*xn)^2形式
有上述逻辑可以得出,因数个数为偶数,仅可能是某一个数的平方这种形式,即完全平方数(1,4,9,16......)。也就是说,整个过程中,只有完全平方数所在的位置的灯会是亮的状态。那么N前面有几个完全平方数,则有几盏灯是亮的,所以仅需要对N开平方,然后向下取整即为最终结果。
import math
def get_light_count(N):
count=int(math.sqrt(N))
print(N,count)
那么我们再对比下两种方法的耗时
暴力破解 | 开平方 | |
100 | 0.0012s | 0.00003.5s |
1000 | 0.1s | 0.00003.5s |
10000 | 9.8s | 0.00003.5s |
这不由得又让我想起了那句话,学好数理化,走遍天下都不怕。数学果然还是优化程序最好的工具。