欧拉工程第一至十题解题思路分析

一、3或5的倍数(multiples of 3 and 5)

如果我们将小于10的所有是3或5倍数的自然数列出来,我们得到3,5,6和9,它们的和是23。与之类似,计算1000以下所有是3或5的倍数的自然数的和。

分析:此题至少有两种解法,第一种解法较为直接,将1000以下所有3或5的倍数列出再求即可,在python中使用列表推导式只需要一行代码即可。第二种思路是使用求和公式,分别求出1000以下所有三的倍数和五的倍数的和再减去十五的倍数的和,即:

$$ s=\sum_{i=1}^{333}3i+\sum_{i=1}^{199}5i-\sum_{i=1}^{66}15i=\frac{3}{2}\cdot333(333+1)+\frac{5}{2}\cdot199(199+1)-\frac{15}{2}\cdot66(66+1) $$

第一种思路的实现代码如下:

def main():
    ans = sum([x for x in range(1,1000) if x%3==0 or x%5==0])
    return ans

二、偶数斐波那契数(even Fibonacci numbers)

斐波那契序列中的数都是由前两项加总得出,假设第一与第二项为1与2,则前十项分别为:

$$ 1,2,3,5,8,13,21,34,55,89 $$
考虑不超过四百万的斐波那契数,计算其中偶数斐波那契数的和。

分析:此题至少有三种解法:第一种我们可以编写一个计算斐波那契数的函数,然后筛选出不超过四百万的数并对其中的偶数求和;第二种思路是我们可以直接得到一个求偶数的斐波那契数的公式,避免筛选的过程;第三种思路我们可以使用斐波那契数列的通项公式,从而可以不使用循环来求和。这里我同时简单介绍下三种思路。

计算斐波那契数是一个经典的编程问题,可以使用多种方法求解,从最简单的递归函数解法,到迭代计算和动态规划的解法,以至矩阵乘法的解法,当然还可以直接用通项公式求解。以上方法中,递归函数的方法效率最低,通常不会使用。而通项公式的算法由于涉及到无理数,存在浮点计算误差的问题,这里我们也不涉及。至于迭代计算、动态规划和矩阵乘法这三种方法在问题规模较小时运行时间没有太大的差别,所以这里我只介绍迭代计算的方法,其它方法相关的详细资料大家可以上网查询,很容易就可以找到。

首先看第一种思路。我们都知道斐波那契数列的定义,在设定了第一项和第二项后(题目假设这两项分别是一和二),之后每一项都等于其前两项之和,因此我们使用临时变量来保存中间的计算过程,并通过迭代计算得到最后的结果。假设开始时\(a=1,b=2\),则在下一步将\(b\)赋值给\(a\),将\(a+b\)的值赋值给\(b\),如此迭代计算实际上就是斐波那契数列的生成过程。要解决题目中的问题,只需要生成一个足够长的斐波那契数列,筛选其中小于四百万的偶数再加总即可。

第二种思路则是考虑到题目只要求对偶数斐波那契数进行加总,这时候求出所有的斐波那契数显得不太经济,有没有办法只求偶数斐波那契数?观察一下上面的斐波那契数列,我们可以发现第二项是偶数,间隔两个奇数第五项也是偶数,再间隔两项第八项也是偶数。之所以会表现出这样的规律,是因为加法的奇偶性法则导致的。我们知道一个奇数加上一个奇数必定是一个偶数,一个偶数加上一个偶数也是一个偶数,然而一个奇数加上一个偶数则必定是一个奇数。在上面的数列中,第一项是奇数,第二项是偶数,则第三项是奇数,第四项等于第二项加上第三项,也就是一个奇数加上一个偶数,所以第四项也是奇数;第五项等于第三项加第四项,这两项都是奇数,所以第五项必定是偶数。依次类推,第八、十一、十四等等项也是偶数。根据题意我们设\(F_1=1,F_2=2\),则有:

$$ \begin{aligned} F_n&=F_{n-1}+F_{n-2}\\ &=F_{n-2}+F_{n-3}+F_{n-3}+F_{n-4}\\ &=2F_{n-3}+F_{n-2}+F_{n-4}\\ &=2F_{n-3}+(F_{n-3}+F_{n-4})+(F_{n-5}+F_{n-6})\\ &=3F_{n-3}+F_{n-4}+F_{n-5}+F_{n-6}\\ &=4F_{n-3}+F_{n-6} \end{aligned} $$

显然,由我们上面的推理可知,\(F_n,F_{n-3},F_{n-6}\)都为偶数,则我们可以把它们表示成为一个新的数列,称为偶数斐波那契数列,第一项为二,第二项为八,递推公式如下:

$$ EF_n=4EF_{n-1}+EF_{n-2}\quad(EF_1=2,EF_2=8) $$

则我们可以根据这个递推公式,使用上面的迭代计算方法计算所有的偶数斐波那契数,省略一半的计算量。

第三种思路是直接使用斐波那契数列的通项公式计算各项斐波那契数,并筛选其中不超过四百万的偶数并求和。这种方法的好处是其时间复杂度为\(O(1)\),是效率最高的算法。坏处是斐波那契数列的通项公式涉及到无理数,从而会出现浮点计算影响精度的问题,当N越大,这种浮点误差就会越严重。虽然我们可以使用更高精度有浮点数或者专门的外部库来解决这个问题,但无疑引入了更多的复杂度,所以这里我们不再详细介绍这种方法。感兴趣的同学可以参见维基百科

第二种思路的实现代码如下:

def main(N=4e6):
    a,b = 2,8
    arr = [a,b]
    while True:
        a,b = b,4*b+a
        arr.append(b)
        if b > N:
            return sum(arr[:-1])

三、最大质因数(largest prime factor)

13195的质因数分别为5,7,13与29,600851475143最大的质因数是多少?

分析:求解质因数的方法较多,最常用的方法为短除法,即对于任意大于2的自然数N,先用N除以2,再用所得之商即(N/2)再除以2,直到商不能为2所整除,此时将被除数加一并比较其平方是否小于被除数,如果小于则再用商除以3,如不能整除,则除以5(因为所有2的倍数即偶数因数已在第一轮不断除以2时被排除了,所以之后都要加二)。这样循环下去直到除数的平方不再小于被除数,则退出循环,最后得到的N即为最大的质因数。实际执行中,要分两层循环,外层循环判断除数的平方是否小于被除数,内层循环判断被除数是否可以整除除数。

def main(n=600851475143):
    i = 2
    while i * i < n:
        while n%i == 0:
            n /= i
        i += 2 if i>2 else 1
    return n

四、最大回文数乘积(largest palindrome product)

回文数即从正反两边读都是一样的数,两个二位数的乘积中最大的回文数为\(9009=91*99\),寻找两个三位数乘积中最大的回文数。

分析:题目说要找到两个三位数相乘得到的最大回文数,所以这个数最小只能是\(100^2\),最大只能是\(999^2\)了,也就是说处于区间\([10000,998001]\)之间,这个区间中绝大部分是六位数,而且题目也要求最大的回文数,所以我们先假设所求数是一个六位数,看能不能找到一个符合要求的数。假设这个六位数是\(abccba\)的形式,则有:

$$ \begin{aligned} 'abccba'&=10^5a+10^4b+10^3c+10^2c+10b+a\\ &=100001a+10010b+1100c\\ &=11(9091a+910b+100c) \end{aligned} $$

可以很明显的看出满足要求的六位数必然是11的倍数,因此在检测六位数是否是回文数之前,我们可以先检测它是否是11的倍数,这样可以加快验证的速度。我们可以使用列表推导式从大到小生成两个三位数的乘积,再检测这个乘积是否是11的倍数以及是否是一个回文数,最后对这个列表求最大值即为所求。需要注意,这里用Lambda表达式定义了一个判断特定数字是否是回文数的函数,方法就是把数字转化为字符串,再判断这个字符串是否与其翻转字符串相等。

def main():
    r = range(999,99,-1)
    is_palindrome = lambda x : str(x) == str(x)[::-1]
    ans = max([i*j for i in r for j in r if (i*j)%11==0 and is_palindrome(i*j)])
    return ans

五、最小公倍数(smallest multiple)

2520是可以被从一到十所有自然数整除的最小的数,即为从一到十的自然数的最小公倍数,求从一到二十所有自然数的最小公倍数。

分析:这道题至少有两种解题思路,第一种思路是编写一个计算最小公倍数的函数,然后对一至二十的所有整数求最小公倍数。在python的math库中有一个计算最大公约数的函数,我们可以根据欧几里德公式,从两个数最大公约数推出两个数的最小公倍数,即对于任意自然数\(a,b\),设两个数的最大公约数为\(gcd(a,b)\),则两个数的最小公倍数

$$ lcm(a,b)=\frac{ab}{gcd(a,b)} $$

据此,我们可以先求出两个数的最小公倍数,再用这个最小公倍数与第三个数求最小公倍数,这里可以利用reduce()函数迭代求出多个数的最小公倍数。这种思路在问题规模较小时表现良好,对题目中的问题也可以在几微秒内给出答案。但是我们可以更深入的分析问题,可以发现一种更为高效的方法。

我们来看一个简化的问题,题目中说一到十的所有整数的最小公倍数是2520,这个数可以如何求出来?我们先对二到十的所有数进行质因数分解,结果如下:

$$ [2:2,3:3,4:2^2,5:5,6:2\times3,7:7,8:2^3,9:3^2,10:2\times5] $$

要想找到一个最小的整除上面所有数的数,我们可以从十以内的所有质数开始寻找,首先看第一个质数二,上面列表中二的最大指数是三,也就是八的质因数分解结果,如果一个数能够被八也就是二的三次方整除,那它必然可以被二和四整除,也就是二的二次方和一次方整除。再来看三,表中三的最大指数是二,那么可以被三的平方整除的数必然可以被三整除。下一个素数是五,表中五的最大指数是一,所以这个数应该整除五。最后一个素数是七,最大指数也是一,所以这个数也应该被七整除。综上,整除表中所有数的最小数应该就是所有数的质因数分解中,各个素数的最高次幂相乘的结果,也就是\(2^3\times3^2\times5\times7=2520\)

显然我们直接利用这个算法来求解题目中的问题,但是我们仍然可以对这个算法做进一步的改进。按照这个算法,我们需要对所有数进行质因数分解来求取各个质数的最大次幂,这个计算相当耗费时间,有没有更快的方法?因为我们只关心各个素数的最大次幂,而不关心各个数的具体质因数分解方式,显然任何素数的最大次幂是使得其不超过终点数的数,比如求一至十的最小公倍数,终点数是十,则二的最高次幂只能是三,而不能是四,因为二的四次方等于十六大于十;同理,三的最高次幂只能是二,因为三的三次方也会大于十。依次类推,五和七的指数都只能是一,因这它们的平方都超过十。在这里我们看到另一个优化技巧,也就是我们只需要求那些其平方不大于终点数的素数的最大次幂,因为那些平方大于终点数的素数,其指数必然是一。最后,根据上面的推理,要求素数的最高次幂,只需要求以该素数为底,以终点数为真数的对数并下取整即可,即有:

$$ p_i^e\le N \Rightarrow e=\lfloor ln(N)/ln(p_i)\rfloor $$

对于题目中的问题规模,第二种算法相对于第二种算法的优势并不明显。在我的电脑上,第一种算法耗时5.5微秒,第二种算法耗时33.6微秒;但当\(N=1000\)时,第一种算法耗时938微秒,第二种算法耗时618微秒,第二种算法时间已经更短。进一步增加问题规模,当\(N=10^4\)时,第一种算法耗时63.5毫秒,第二种算法耗时7.22毫秒;当\(N=10^5\)时,第一种算法耗时5.85秒,第二种算法耗时198毫秒,两者的效率差距已经差别很大。总体而言,第一种算法的时间复杂度接近\(O(n^2)\),第二种算法的时间复杂度接近\(O(nlogn)\)。两种算法的实现代码如下:

# approach 1, time complexity = O(n^2)

from math import gcd
from functools import reduce

def lcm(n):
    def lcm(a, b):
        return (a * b) // gcd(a, b)
    return reduce(lcm, range(1,n+1))

# approach 2, time complexity = O(nlog(n))

from sympy import primerange
from math import sqrt,log,floor

def main(n=20):
    primes = list(primerange(1,n))
    i,ans = 0,1
    while primes[i] < sqrt(n):
        e = floor(log(n)/log(primes[i]))
        ans *= (primes[i])**e
        i += 1
    for p in primes[i:]:
        ans *= p
    return ans

六、和的平方与平方的和之间的差值(Sum square difference)

前十个自然数的平方的和为:

$$ 1^2+2^2+3^2+\cdots+10^2=385 $$
而前十个自然数和的平方为:
$$ (1+2+3+\cdots+10)^2=55^2=3025 $$
两者的差为\(3025-385=2640\),求前一百个自然数的和的平方与平方的和之间的差值。

分析:此题可以直接使用求和公式求解,则:

$$ S(n)=(\frac{1}{2}n(n+1))^2-\frac{1}{6}n(n+1)(2n+1)=\frac{1}{12}n(n-1)(n+1)(3n+2) $$

代入\(S(100)\)可以直接算出结果,代码如下:

def main(n=100):
    ans = n*(n-1)*(n+1)*(3*n+2)/12
    return int(ans)

七、第10001个质数(10001st prime)

列出前六个质数,我们可以发现第六个质数为13,那么第10001个质数是多少?

分析:一般而言,对于第\(n\)个素数\(p_n\),满足以下不等式(参见素数计数函数):

$$ ln(nln(n))-1<\frac{p_n}{n}<ln(nln(n)) $$

在这里我们只关注上界,则可以得到\(p_n<nln(nln(n))=nln(n)+nln(ln(n))\),我们先筛选出从2到上界的所有质数,再取其中的第10001个质数,即得到结果。

from sympy import primerange
from math import log,ceil

def main(n=10001):
    upper_bound = ceil((log(n)+log(log(n))) * n)
    primes = list(primerange(1,upper_bound))
    return primes[n-1]

八、序列中的最大乘积

在下面1000位数中,连续四个数的最大乘积为\(9*9*8*9=5832\):

IMG

寻找连续十三数的最大乘积,这个乘积是多少?

分析:此题解题思路较为直接,分为以下步骤:1)将以上数字存入到TXT文件中,用python导入并存入到一个LIST中;2)从LIST中第0位开始直到第987位,依次求连续十三位数的乘积并存入到结果LIST中;3)求结果LIST的最大值得到结果。

from functools import reduce

def main():
    with open('euler/ep08.txt','r') as f:
        data = ''
        for line in f.readlines():
            data = data + line.strip()
    res = []
    for i in range(988):
        sub = [int(x) for x in data[i:i+13]]
        prod = reduce(lambda x,y:x*y, sub)
        res.append(prod)
    return max(res)

九、特殊的毕达哥拉斯三元数

毕达哥拉斯三元数是指一类三个自然数的集合,其中\(a<b<c\)\(a^2+b^2=c^2\),例如\(3^2+4^2=5^2\)。仅存在一组毕达哥拉斯三元数使得\(a+b+c=1000\),求\(abc\)

分析:此题至少有两种解题思路,第一种思路如下:为了让解题思路更加一般化,设三角形的周长为\(p\),则有\(a+b+c=p\),故\(c=p-a-b\),则有:

$$ a^2+b^2=c^2=(p-a-b)^2=p^2+a^2+b^2-2pa-2pb+2ab $$

则可以得到:

$$ b=(p^2-2pa)/(2p-2a) $$

则使得\(b\)为整数的\(p\)\(a\)都可以满足题目的要求,从而可以得到一组符合要求勾股三元数。此外,不失一般性,我们假设\(a\le b<c\),又因为\(a+b+c=p\),则应有\(a<p/3\),这可以缩小我们需要筛选的数\(a\)的范围。因为题目已经说明在\(p=1000\)时只存在一组勾股三元数,则我们只需要编写一个函数寻找一个合适的\(a\)值使得\(b\)为整数,一旦找到就可以计算\(c\)并返回三者的乘积。

第二种思路需要用到欧几里德公式,对于\(m>n>0\),我们知道以下三个数可以构成勾股三元数:

$$ a=k(m^2-n^2),b=2kmn,c=k(m^2+n^2) $$

则有:

$$ a+b+c=k(m^2-n^2+2mn+m^2+n^2)=k(2m^2+2mn)=2km(m+n)=1000 $$

则我们可以得到\(m(m+n)=500/k\),显然\(m<m+n\),同时\(m,n\)都是\(500/k\)的因数,则我们可以得到\(m<\sqrt{500/k}\)。同时我们有\(n=500/k\cdot m-m<m\),则有\(m>\sqrt{250/k}\),即我们可以是得到\(m\)的取值范围是\([\sqrt{250/k},\sqrt{500/k}]\)。现在我们假设\(k=1\),则\(m\in[15.81,22.36]\),这其中的整数只有16,17,18,19,20,21,显然只有20是500的因子,则有\(m=20,n=5\),则有:

$$ a=20^2-5^2=375,b=2\times20\times5=200,c=20^2+5^2=425 $$

\(a,b,c\)三者相乘即为题目所求。显然第二种思路可以直接笔算出结果,所以这里我们只列示第一种思路的代码:

def main(p=1000):
    for a in range(1,p//3):
        n,d = p**2-2*p*a,2*p-2*a
        if n % d == 0:
            b = n/d
            c = p-a-b
            return int(a*b*c)

十、质数的和

\(10\)以下的质数的和为\(2+3+5+7=17\),求所有两百万以下的质数的和。

分析:对此题的详细分析请参见我的另一篇文章