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

四十一、全数字素数(pandigital prime)

当一个N位数的数字各个数位分别使用了1至N所有的数字,我们就说这是一个N位的全数字。例如,2143是一个四位的全数字,同时还是一个素数。求最大的同时为素数和N位全数字的数。

分析:小学的时候我们都已经学过,当一个数字的所有数位之和是三的倍数,则这个数可以被三整除,因此不可能是一个素数。我们可以试着计算所有两位数以上的全数字各位数之和:

$$ \begin{aligned} 1+2&=3\\ 1+2+3&=6\\ 1+2+3+4&=10\\ 1+2+3+4+5&=15\\ 1+2+3+4+5+6&=21\\ 1+2+3+4+5+6+7&=28\\ 1+2+3+4+5+6+7+8&=36\\ 1+2+3+4+5+6+7+8+9&=45 \end{aligned} $$

我们可以很明显看到,只有四位和七位全数字的各位数之和不是三的倍数,因此只有这两个数位的全数字中可能存在素数。考虑到七位数必然大于四位数,所以我们从最大的七位全数字7654321开始筛选,依次寻找一个更小的素数,然后判断这个素数是否为全数字。判断的方法是检查该数字对应的字符串构成的集合是否等于由字符串1234567构成的集合,如果等于则是一个全数字,如果不等于就不是。我们首先在七位数的范围内寻找满足要求的数,如果找不到,再在四位数范围内寻找。经过尝试,我们在七位数的范围内就能找到,所以无需再在四位数范围内寻找。

from sympy import prevprime

def main():
    start = 7654321
    while True:
        prevp = prevprime(start)
        if set(str(prevp)) == set('1234567'):
            return prevp
        else:
            start = prevp      

四十二、编码三角数(coded triangle numbers)

三角数数列的第\(n\)项可以通过公式\(t_n=n(n+1)/2\)来确定,所以前十个三角数分别为:

$$ 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ... $$
通过把一个单词中的每个字母转化为其在字母表中的位置序数,并将这些数相加可以形成一个单词值。例如,单词SKY的单词值为\(19 + 11 + 25 = 55 =t_{10}\)。如果一个单词的单词值是一个三角数,我们就称这个单词是三角单词。

在题目给出的words.txt这个16K的文本文件中有单约两千个单词,这些单词中有多少个是三角词?

分析:这道题相对容易,首先我们需要编写一个函数计算每个单词对应的单词值,首先计算字母在字母表的序数值,这里使用了内置函数ord(),它可以计算字符对应的ASCII值,用特定字母的ASCII值减去字母A的ASCII值即为该字母在字母表的顺序值,然后将这些顺序值加总起来即为每个单词的单词值。在计算出单词值后,如何判断其是否为一个三角数?

假设计算出的单词值是\(t\),则将其代入三角数的通项公式,则\(t=n(n+1)/2\),则解此二次方程并略去负数项,我们可以得到:

$$ n=\frac{-1+\sqrt{1+8t}}{2} $$

因此当且仅当\(\sqrt{1+8t}\equiv1(mod\ 2)\)时,\(n\)是一个正整数,满足题目的要求。我们根据这个条件对单词值进行筛选,统计筛选后满足条件的单词数量,即为所求。

from math import sqrt

def alphabet(word):
    alpha = lambda s : ord(s) - ord('A') + 1
    res = sum([alpha(x) for x in word])
    return res

def main():
    with open('euler/ep42.txt','r') as f:
        data = f.read().replace('"',"").split(',')
    word_value = [alphabet(x) for x in data]
    res = [x for x in word_value if sqrt(1+8*x)%2==1]
    return len(res)

四十三、子串可整除性(sub-string divisibility)

数字1406357289由0至9的各位数构成,因此是一个0至9的全数字,除此之外,它还有一个有趣的子串可整除性质。设\(d_1\)表示第一位数,\(d_2\)表示第二位数等等,我们可以发现:

  • \(d_2d_3d_4=406\)可以被\(2\)整除
  • \(d_3d_4d_5=063\)可以被\(3\)整除
  • \(d_4d_5d_6=635\)可以被\(5\)整除
  • \(d_5d_6d_7=357\)可以被\(7\)整除
  • \(d_6d_7d_8=572\)可以被\(11\)整除
  • \(d_7d_8d_9=728\)可以被\(13\)整除
  • \(d_8d_9d_{10}=289\)可以被\(17\)整除

求所有满足这个性质的0至9的全数字之和。

分析:这道题有两种解法,第一种解法利用题中所列素数的整除性质,并遍历所有0至9全数字的组合寻找满足要求数字并累加求和。第二种方法则需要在整除性质的基础上,对各个数位的出现数字的可能性加以分析,迭代解出满足要求的数字,使用这种方法可以直接笔算出相应结果,不需要编程。相对来说,第一种方法更好理解,但效率较低;第二种方法效率更高,但理解起来要麻烦一些。这里我同时介绍两种方法。

首先看第一种方法,根据题目条件分析各个数位应该满足的性质:

根据以上条件,我们可对所有\(10!=3628880\)个全数字进行筛选,并将最后筛选剩下的数字加总,即为所求。这个方法在我的电脑上耗时5.3秒。

第二种方法也需要用到各位数的整除性质,我们首先看\(d_6\),通过上面的分析,我们知道\(d_6\)只能取0或5。假设\(d_6=0\),则\(d_6d_7d_8\)要被11整除,则只能是\(022,033,044\cdots\)这样的形式,这显然和全数字的要求违背,所以\(d_6\)只能等于5。在已知\(d_6=5\)的情况下,满足\(d_6d_7d_8\)被11整除的数只有八个,分别是506,517,528,539,561,572,583,594。以这八个数为起点,考虑\(d_7d_8d_9\)需要被13整除,则可以进一步把满足要求的数缩减至四个,分别是5286,5390,5728,5832。以这四个数为基础,考虑\(d_8d_9d_{10}\)需要被17整除,则满足要求的数缩减至三个,分别52867,53901,57289,这是全数字最后五个数的可能选择。让我们回过头往前看,\(d_5d_6d_7\)被7整除,其中\(d_6d_7\)只有52、53和57三个选择,经过尝试发现只有952和357两个数字满足条件,则目前只剩下952867和357289两个选择。接下来\(d_3d_4d_5\)必须被3整除,同时\(d_4\)是一个偶数,\(d_5\)只有3和9两个选择,最后筛选的结果是只有三个数字满足条件,分别是30952867、 60357289和06357289。剩余前两位数,只有1和4两个选择,共有两种组合,这两种组合加上三个可能数字,对应最终的六个解,分别为1430952867、1460357289、1406357289、4130952867、4160357289 和 4106357289。

from itertools import permutations

def main():
    res,ans = [],0
    for i in permutations(range(10),10):
        d_by_three = (i[2]+i[3]+i[4])%3==0
        d_by_seven = (10*i[4]+i[5]-i[6]*2)%7==0
        d_by_eleven = (i[5]-i[6]+i[7])%11==0
        d_by_thirteen = (100*i[6]+10*i[7]+i[8])%13==0
        d_by_seventeen = (100*i[7]+10*i[8]+i[9])%17==0
        cond = d_by_three and d_by_seven and d_by_eleven and d_by_thirteen and d_by_seventeen
        if i[0]!=0 and i[5]==5 and i[3]%2==0 and cond:
            ans += sum([x*10**y for x,y in zip(i,range(9,-1,-1))])
    return ans

四十四、五边形数(pentagon numbers)

五边形数可以由以下公式生成:\(P_n=n(3n-1)/2\),前十个五边形数如下:

$$ 1, 5, 12, 22, 35, 51, 70, 92, 117, 145, ... $$
可以看出\(P_4+P_7=22+70=92=P_8\)。然而,这两个数的差\(70-22=48\)并不是五边形数。假设有一对五边形数\(P_j\)\(P_k\),这两个数的和和差的绝对值都是五边形数,假设\(D=|P_k-P_j|\),求\(D\)的最小值。

分析:这道题的难度并不在于求出一个\(D\),而在于证明求出来的这个\(D\)是最小的,很多网络文章上的算法都忽视了这一点,默认算出来的第一个\(D\)就是最小的,而没有给出令人信服的证明。我只用了十几分钟就求出了答案,但是花了几天时间不断修改算法,才最终让我相信求出来的第一个\(D\)必然是的最小的\(D\)。看到这个题的第一个想法就是生成一个足够长的五边形数的列表,将列表中的数两两组合求其和与差,当和与差都为五边形数即满足了题目要求,差的绝对值即为所求。经过尝试,\(P_{2167}\)\(P_{1020}\)是满足条件的第一对五边形数,两者的差值就是题目的答案,但问题是如何证明这个差值就是最小的?

很多网络文章的思路是这样的,观察到相邻两个五边形数之间的差值在逐渐增大,事实上相邻两个五边形数的差值\(m=P_k-P_{k-1}=3k-2\)确实会随着\(k\)的增加而增加。因此,他们认为如果我们求出了一个\(D_1\),那么之后的\(D_2,D_3\cdots D_n\)都应该比\(D_1\)大,但是这种论证思路是不成立的。假设我们求出了一个\(D=P_k-P_j\)(\(k>j\)),虽然我们知道\(P_k\)会随着\(k\)的增大而增大,但是这并不能排除一对满足要求的值\(P_m\)\(P_n\),其中\(P_m>P_k\),但\((m-n)<(k-j)\),从而使得求出的\(D_{mn}<D_{kj}\)。为了完全排除这种可能性只有一种方法,那就是让\(k\)变得足够大,以使得相邻两个五边形数的差值都大于\(D\),如果经过验算表明在小于\(k\)的范围内\(D\)确实是最小值,而大于\(k\)的范围内不存在差值会小于\(D\),则\(D\)必然应是全局最小值。在已知答案的情况下,我们可以试着反推出这个足够大的\(k\)值,我们设相邻两个五边形数的差值大于题目的答案也就是最小的\(D\)值,即\(3k-2>5482660 \Rightarrow k>1827554\),这是一个非常大的值,验证在小于这个的数的范围内是满足要求的最小值要花很长的时间,至少在我的电脑上,我没法在可以接受的时间内验证这个想法。

因此,我们需要换一个思路,不从数对\(P_k\)\(P_j\)入手,而是从两者的和与差值入手。因为按照题目要求满足要求的数其和与差也是五边形数,所以我们只需要在这些五边形数中从小到大依次确定一个\(D\)值,再反推出相应的和,如果和与差都是五边形数,则是满足条件的数。因为我们是从小到大顺序来验证\(D\)值的,所以我们遇到的第一个满足要求的\(D\)值就必然是全局最小值,上面介绍的难题可以迎刃而解。具体算法是这样的,假设\(k>j\)\(P_k+P_j=S\),且有\(P_k-P_j=D\),则有\(S=D+P_j+P_j=D+2P_j\),这样当我们确定了\(D\)\(P_j\)后,只需要进一步确定\(D+P_j\)\(D+2P_j\)也是五边形数即找到了题目要求的数值。需要注意的是,我们并没有充分的理由相信\(P_j<D\),所以我们做验证的时候会先假设\(P_j<D\)算出\(D+2P_j\)进行验证,然后将两个数反转过来计算\(P_j+2D\)并验证,这样可以确保我们考察了所有可能的\((D,P_j)\)数对。

上面的问题弄清楚以后,剩下的只是一些平凡优化的技巧了。例如,为了确定一个数是否是五边形数,我们求出五边形数通项的反函数,即\(n=(1+\sqrt{24x+1})/6\),所以只有当且仅当我们得到\(1+\sqrt{24x+1}=0(mod\ 6)\)时,数\(x\)才是五边形数,使用这种方法,可以更快的判断一个数是否是五边形数。

from math import sqrt

def is_pantagon(x):
    if (sqrt(24*x+1)+1) % 6 == 0:
        return True
    return False

def main():
    pantagon = lambda n : n*(3*n-1)/2
    pt = [pantagon(n) for n in range(1,2000)]
    for i in range(1,2000):
        for j in range(0,i):
            s1,s2 = pt[i] + 2*pt[j], 2*pt[i] + pt[j]
            pk = pt[i] + pt[j]
            if is_pantagon(pk):
                if is_pantagon(s1):
                    return pt[i]
                if is_pantagon(s2):
                    return pt[j]

四十五、三角形数,五边形数和六边形数(Triangular, pentagonal, and hexagonal)

三角形数,五边形数和六边形数可分别由以下公式生成:

  • 三角形数:\(T_n=n(n+1)/2 \qquad 1,3,6,10,15\cdots\)
  • 五边形数:\(P_n=n(3n-1)/2 \qquad 1,5,12,22,35\cdots\)
  • 六边形数:\(H_n=n(2n-1) \qquad 1,6,15,28,45\cdots\)

可以论证\(T_{285}=P_{165}=H_{143}=40755\),求下一个同时为五边形数和六边形数的三角形数。

分析:这道题目相对比较简单直接。首先可以观察到一个六边形数即是一个奇数位置的三角数,比如1是第一个三角形数,6是第三个三角形数,15是第五个三角形数。事实上,对任意奇数\((2n-1)\),将其代入三角形数的通项有:\((2n-1)(2n-1+1)/2=n(2n-1)\)即是一个六边形数。所以六边形数只是三角形数的子集,当一个数是六边形数,我们无需再验证它是否是三角形数。在上一道题即第四十四题中,我们已经编写了一个判断一个数是否为五边形数的函数,这里可以直接复用。我们从\(n=144\)开始依次生成六边形数,再判断生成的数是否为五边形数,如果不是再生成下一个数,如果是则返回该六边形数,即为所求。

from math import sqrt

def is_pantagon(x):
    if (sqrt(24*x+1)+1) % 6 == 0:
        return True
    return False

def main():
    i = 144
    while True:
        h = i*(2*i-1)
        if is_pantagon(h):
            return h
        else:
            i += 1

四十六、另一个哥德巴赫猜想(Goldbach’s other conjecture)

克里斯蒂安·哥德巴赫曾经提出任何一个奇合数都可以被写成一个质数和一个平方数的两倍的和,如:

$$ \begin{aligned} 9 &= 7+2\times1^2\\ 15 &= 7+2\times2^2\\ 21 &= 3 + 2\times3^2\\ 25 &= 7 + 2\times3^2\\ 27 &= 19 + 2\times2^2\\ 33 &= 31 + 2\times1^2 \end{aligned} $$
然而人们最终发现这个猜想是错误的,求最小的不能被写成一个质数和一个平方数的两倍之和的奇合数。

分析:简单分析一下题意,我们可以有两种思路:第一种思路是对任意的奇合数\(c\),找到所有小于它的质数\(p\),用这个奇合数分别减去这些质数再除以二,判断结果是否是一个平方数。根据素数定理,对任意正实数\(x\),设\(\pi(x)\)为小于\(x\)的素数个数,则\(\pi(x)\approx x/ln(x)\),则需要判断的次数也等于这个数。第二种思路是对任意奇合数\(c\),设其满足题目要求,则\(c=p+2k^2\),因为素数必大于一,即\(p=c-2k^2>1 \Rightarrow k<\sqrt{(c-1)/2}\),简单观察可以发现\(k<\pi(x)\),因此使用第二种方法需要验证的次数要小于第一种方法的验证次数。比如说对于奇合数5001,小于它的质数共有669个,因此使用第一种方法要做669次验证,而使用第二种方法只需要做不超过\(\sqrt{(5001-1)/2}=50\)次验证。我们需要编写一个验证一个奇合数是否存在满足题目要求的分解方法的函数,即对小于\(k\)的值逐一验证判断奇合数减去一个平方的二倍是否为一个质数,如果存在一个这样的质数,则退出循环。如果验证完所有小于\(k\)的值仍然未找到这样一个质数,则该奇合数不能表示题目要求的形式,即为所求。以下是对\(k<\pi(x)\)的严格证明,如果对此不感兴趣可以跳过,直观上知道第二种思路的验证次数更少就可以了。

为了证明\(k<\pi(x)\),考虑到至少在\(10^{25}\)范围内,\(\pi(x)>x/ln(x)\),则只需要证明:

$$ \sqrt{(x-1)/2}<x/ln(x) $$

利用对数平均不等式,即对所有的\(x\ge0,y\ge0\),有:

$$ \sqrt{xy}\le\frac{x-y}{lnx-lny}\le \frac{x+y}{2} $$

则有:

$$ \sqrt{\frac{x-1}{2}}<\sqrt{\frac{x}{2}}<\sqrt{x}<\frac{x-1}{lnx}<\frac{x}{lnx} $$

证明完毕。

from math import sqrt
from sympy import isprime

def nonexist(x):
    limit = int(sqrt((x-1)/2))
    for i in range(1,limit+1):
        if isprime(x-2*i**2):
            return False
    return True

def main():
    i = 35
    while True:
        if not isprime(i) and nonexist(i):
            return i
        i += 2

四十七、不同的质因数(distinct primes factors)

第一个有两个不同质因数的两个相邻整数是:

$$ 14=2\times7\\ 15=3\times5 $$
第一个有三个不同质因数的三个相邻整数是:
$$ \begin{aligned} 644&=2^2\times7\times23\\ 645&=3\times5\times43\\ 646&=2\times17\times19 \end{aligned} $$
找到第一个有四个不同质因数的四个相邻整数,求这四个整数中的第一个。

分析:首先我们可以想到一个整数如果要有四个不同的质因数,它至少应该大于2,3,5,7的乘积也就是210,因此最先想到的思路是从210开始逐个求出每个合数的质因数个数,如果连续发现了四个有四个不同质因数的合数,则其中第一个数就是题目的答案。但是我们还可以进行进一步优化,考虑相邻两个质数之间的间隔,如果这个间隔小于或等于四,则这个间隔内无法放下四个连续的合数,因此这个间隔中的合数肯定不满足题目要求,这样我们可以排除掉一大批合数,缩小筛选的范围。对于间隔大于四的两个质数,我们对间隔中的合数逐个求其质因数个数,如果发现了四个连续的有四个不同质因数的合数,则其中第一个即为所求。如果在这些合数中没有找到,则我们再以下一个质数为起点,重复上面的过程,只到找到满足要求的四个合数。我们首先编写一个函数计算特定列表中满足要求的第一个数,如果有则返回它,如果没有则返回假。然后利用上面的思路,在相邻质数之间的合数中来寻找满足要求的数。代码如下:

from sympy import primefactors,nextprime

def first_int(arr):
    f = lambda x : len(primefactors(x))
    for i in range(len(arr)-3):
        if f(arr[i])==4 and f(arr[i+1])==4 and f(arr[i+2])==4 and f(arr[i+3])==4:
            return arr[i]
    return False

def main():
    p = 211
    while True:
        nextp = nextprime(p)
        if (nextp - p) > 4:
            ans = first_int(range(p+1,nextp))
            if ans != False:
                return ans
        p = nextp

四十八、自身的幂(self powers)

数列\(1^1+2^2+3^3+\cdots+10^{10}=10405071317\),求\(1^1+2^2+3^3+\cdots+1000^{1000}\)的最后十位数。

分析:此题的解法非常直接,不需要做太多的分析,直接求出题目中给出的指数和,再求其模\(10^{10}\)就可以得到其最后十位数。代码如下:

def main():
    res = sum([x**x for x in range(1,1001)])
    return (res % 10**10)

四十九、素数排列(prime permutations)

算术序列1487,4817,8147其中后一项在前一项的基础上增加了3300,这个序列有两个独特的性质:i)三个数都是素数;ii)每个数的四位数都是另外两个数字四位数的重新排列。没有一位、两位和三位素数组成的算术序列满足上面的性质,但是还有一组满足这个性质的四位数递增序列。这个序列的三个数前后拼接组成的十二位数是多少?

分析:此题的解法比较直接,题目说了满足条件的三个数是四位的素数,同时每个数都是另外两个数的重新排列,这意味着三个数的各位数构成的集合应该相同。所以我们只需要筛选从一千到一万的四位数素数,将素数加上3300得到第二个数,再加上6600得到第三个数,如果这两个数都是素数且构成三个数的各位数的集合相等,则是满足条件的数。最后将三个数转化成字符串前后相加拼接,即为题目所求。代码如下:

from sympy import isprime,primerange

def main():
    primes = primerange(1000,10000)
    for p in primes:
        if p != 1487:
            a = p + 3330
            b = p + 6660
            if isprime(a) and isprime(b) and set(str(p))==set(str(a))==set(str(b)):
                return str(p) + str(a) + str(b)

五十、连续素数的和(consecutive prime sum)

素数41可以写成六个连续素数的和:

$$ 41 = 2 + 3 + 5 + 7 + 11 + 13 $$
这是一百以下可以被写成连续素数的和中包含素数最多的。一千以下可以被写成连续素数的和,并且包含素数最多的素数是953,这个和中包含21个素数。求一百万以下可以被写成包含最多的连续素数的和的素数。

分析:先考虑最简单的情况,如何求出一个一百以下的素数,它可以表示为连续素数的和并且这个和包含最多的素数?题目已经给出了这个数是四十一,可以表示为从二至十三的连续素数的和,包含的素数个数为六个。看到这个问题,我们首先可以想到从二开始不断累加素数,加到这个和刚好超过一百为止,这时候得到素数序列为二到二十三的素数,我们截去最后一个素数,得到的素数序列即为其总和不超过一百的最长素数序列,也即[2, 3, 5, 7, 11, 13, 17, 19]。这个序列包含的素数有八个,其总和为77,显然不是素数。然后我们分析七个连续素数的情况,这只能包含两个序列,即[2, 3, 5, 7, 11, 13, 17]和[3, 5, 7, 11, 13, 17, 19],两个列表的和也不是素数,仍然排除。然后我们观察六个连续素数,则有[2, 3, 5, 7, 11, 13]、[3, 5, 7, 11, 13, 17]和[5, 7, 11, 13, 17, 19]三种情况,对应的和值分别为41, 56和72,显然只有41是素数,则我们可以得出结论41是一百以下可以表示为连续素数的和并且这个和包含最多的素数的素数,包含的素数个数为六个。

对于一百万以下的素数,我们也可以采用类似的思路,即首先产生一个从二开始的连续素数列表,其总和应该刚好要超过一百万。然后我们使用两个循环,外循环确定素数个数,从列表的长度减一逐渐递减到零;内循环确定连续素数的起始素数,从列表中第一个元素也就是二开始逐渐递增。然后观察这个新列表的和是否为一个素数,如果得到了一个素数,则可以直接返回结果,即为题目所求;如果没有,则继续循环,只到找到一个满足要求的素数。需要注意的是,我们额外编写了一个函数确定合理的素数上界是多少,通过单独编写函数将这一部分逻辑分离出来,可以使得主函数的结构更加清晰,这一部分逻辑也可以更好的复用。

from sympy import isprime,primerange,nextprime

def primesum_below_N(N):
    start = 2
    arr = [start]
    while True:
        nextp = nextprime(start)
        arr.append(nextp)
        if sum(arr)>=N:
            return arr[:-1]
        start = nextp

def main(N=10**6):
    primes = primesum_below_N(N)
    length = len(primes)
    for d in range(length-1,0,-1):
        for start in range((length-d+1)):
            res = sum(primes[start:start+d])
            if isprime(res):
                return res