0200 平立方强合数
* * * *
拉格朗日计划
* * * *
平立方强合数

将形如$p^2q^3$的数称为平立方数,其中$p,q$是不同的素数。

例如,$200=5^2\cdot2^3$,$120072949= 23^2\cdot61^3$都是平立方数。前五个平立方数分别是72, 108, 200, 392和500。

此外,200还是第一个无法通过只改变一位数字就变成素数的合数, 让我们称这样的数为强合数。下一个包含子串200且是强合数的平立方数是1992008。

找出第200个包含子串且是强合数的平立方数。

注:强合数在原题中的原文为prime proof number,据考证也是欧拉计划所起的名称,并非普遍接受的学术名词。但数论中有所谓的弱素数的提法,因而此处意译为强合数。

本题难度:



解答

首先用筛法生成一定范围内的素数,这一范围可以通过多次实验合理选择,由于撰写本文时已经算出了结果,因此这里选择上界为二十万,大约有一万八千个素数。

接下来需要有序生成平立方数,这可以用一个堆来实现,堆的长度始终与上一步素数表的长度一致,堆中元素的形式为$(z,i,j)$,其中$z=p_i^3p_j^2$,$p_i,p_j$分别是第i个和第j个素数。

每次弹出堆顶元素$(z,i,j)$,就是下一个平立方数,然后只要$i\neq j+1$,就把$(p_i^3p_{j+1}^2,i,j+1)$压入堆,否则压入$(p_i^3p_{j+2}^2,i,j+2)$。

最后需要判断是否是强合数,这涉及到数字的变化和素性检验。

注意到改变一个数的某位数字就是把这个数加上$d\cdot 10^k$,其中$d$是$[-9,9]$之间的整数,例如把231变为271就是把231加上40。

因此若z能被2或5整除,那么改变任意十位及以上的数字所得都还是合数,只需检验将个位数变为1、3、7、9的情况(事实上大多数强合数都是2或5的倍数,见OEIS A118118)。

若z不能被2或5整除,则再逐一改变其各位数字并作素性检验。

素性检验采用混合模式,由于已经生成了二十万以内的素数,因此小于二十万的数直接查表,二十万到四百亿之间的数用该表中的素数逐个作整除检验,超过四百亿的用Miller-Rabin。

最终结果是$229161792008=2^3\cdot 169249^2$。

注:出乎本人意料,本题初看计算量惊人,但实际程序几乎瞬间就给出了结果。

import heapq
target=200000

d=[0]*target
n=2
while n < target:
    for i in range(n*n,target,n):
        d[i]=d[i]+1
    n=n+1
    while n < target and d[n]>0:
        n=n+1

primes=[k for k in range(2,target) if d[k]==0]
primeSet=set(primes)
n=len(primes)

seeds=[2,325,9375,28178,450775,9780504,1795265022]

def quickPower(x,y,q):
    if y <= 5:
        return (x**y)%q
    else:
        z=quickPower(x,y/2,q)
        if y%2:
            return (z*z*x)%q
        else:
            return (z*z)%q

def millerRabin(m):
    if m <= target:
        return m in primeSet
    elif m <= target*target:
        k=0
        while k < n and primes[k]*primes[k] <= m and m%primes[k]:
            k+=1
        return k>=n or m%primes[k]>0
    elif m%2==0:
        return False
    else:
        x=m-1
        while x%2==0:
            x=x>>1
        for s in seeds:
            a=quickPower(s,x,m)
            c=x
            while c < m-1 and a!=1 and a!=m-1:
                a=(a*a)%m
                c=c << 1
            if c>m-1 or (a==1 and c>x):
                return False
            else:
                a=(a*a)%m
                c=c << 1
                while c <= m-1:
                    if a!=1:
                        return False
                    a=(a*a)%m
                    c=c << 1
        return True

def check(z):
    if any((millerRabin(z-z%10+1),millerRabin(z-z%10+3),millerRabin(z-z%10+7),millerRabin(z-z%10+9))):
        return False
    else:
        if z%2==0 or z%5==0:
            return True
        else:
            u=str(z)
            for i in range(1,len(u)):
                w=10**i
                v=z-int(u[-i-1])*w
                if any(millerRabin(v+j*w) for j in range(10)):
                    return False
            return True


h=[(72,0,1)]+[(primes[i]*primes[i]*primes[i]*4,i,0) for i in range(1,n)]
heapq.heapify(h)
r=[]
while len(r) < 200:
    z,i,j=heapq.heappop(h)
    j+=1
    if i==j:
        j+=1
    heapq.heappush(h,(primes[i]*primes[i]*primes[i]*primes[j]*primes[j],i,j))
    if "200" in str(z) and check(z):
        r.append(z)

print r[-1]