求两个平方数的第 n 个最小和

find nth smallest sum of two squares numbers

提问人:user6703592 提问时间:12/13/2022 最后编辑:user6703592 更新时间:12/15/2022 访问量:184

问:

它是一种算法编码:x = a^2 + b^2,a,b 为正整数。求第 n 个最小的 x。

我们知道 f(1) = 1^2 + 1^2;f(2) = 1^2 + 2^2;f(3) = 2^2 + 2^2....

我的想法是通过比较 (a+1)^2 + b^2 与 a^2 + (b+1)^2 来更新 a,b。但事实并非如此,f(4) = 1^2 + 3^3,这不是从 2^2 + 2^2 更新的。

我们能找到比暴力枚举更好的算法(O(n^2))吗?

算法

评论

0赞 Stef 12/13/2022
@MrSmith42 考虑所有可以写成 a²+b² 的数字的(无限)序列,按递增顺序排列。OP 要求提供该序列中的第 n 个数字。例如,序列中的第一个项是 0、1、2、4、5、8、9,因此第 0 项是 0,第 1 项是 1,第 2 项是 2,第 3 项是 4,依此类推
0赞 Dmitry Bychenko 12/13/2022
您可能想找到最小的:x' 是nx(1, 1) (2, 1) (2, 2) (3, 1) (3, 2) (4, 1) (3, 3)..., so the 5th smallest 13 (3^2 + 2^2)
0赞 Stef 12/13/2022
换句话说,OP 要求该序列中的第 n 项:oeis.org/A001481
2赞 Dmitry Bychenko 12/13/2022
大概 oeis.org/A000404
2赞 user6703592 12/13/2022
@PranavHosangadi 我们能找到比暴力枚举更好的算法(O(n^2))吗?

答:

1赞 Stef 12/13/2022 #1

第一个算法想法是使用堆。

凭直觉,您知道最小的 a 和 b 是最小的,最小的 a²+b² 将是。所以:

  • 一目了然,10² + 3² < 10² + 4²
  • 一目了然,10²+3²是否<?7²+8²

我建议将 (a,b) 对添加到最小堆中,慢慢增加 a 和 b。然后,序列中的下一个元素是堆中的下一个元素。

在下面的算法中,表示整数除以 2;例如,和 都等于 。/210/211/25

function nth_sum_of_two_nonzero_squares(n):
    heap <- empty min-heap
    for k in range [2 .. n+1]:
        for a in range [1 .. k/2]:
            push a² + (k-a)² to heap if it's not already in the heap
        next_term <- pop(heap)
    return next_term

略有改进:为给定添加的最小项是 。我们可以利用该值的知识来避免向堆添加更多候选者,只要堆的当前根仍然小于下一个候选者。ka = k / 2

这为我们提供了:

function nth_sum_of_two_nonzero_squares(n):
    heap <- empty min-heap
    i = 0
    k = 2
    push 1²+1² to heap
    loop forever:
        next_candidate = ((k+1)/2)² + ((k+2)/2)² = ((k+1)²+1)/2
        while next_candidate > peek(heap):
            i <- i + 1
            next_term = pop(heap)
            if i == n:
                return next_term
        k <- k + 1
        for a in range [1 .. k/2]:
            if a² + (k-a)² not already in heap:
                push a² + (k-a)² to heap
        i <- i+1
        next_term = pop(heap)
        if i == n:
            return next_term

缺点是,我们为弹出的每个术语向堆中添加越来越多的术语。在这里,堆的大小看起来是二次的。但是,我确信 的最大值是 的平方根的量级。如果这是真的,那么算法只使用伪线性时间和线性空间。kkn

您可以通过尝试为弹出的每个术语向堆中添加更少的术语来改进此算法。这需要更多的直觉来了解如何以 a²+b² 的伪递增顺序对 (a, b) 进行排序。

在 python 中测试并与 OEIS 进行比较:正确性为 2 个非零平方之和的数字:

from heapq import heappush, heappop
from itertools import count, islice

def sums_of_two_nonzero_squares():
    heap = [2] # 2 == 1² + 1²
    seen = {2}
    for k in count(3):
        next_candidate = (k*k + 1)//2
        while heap and heap[0] < next_candidate:
            next_term = heappop(heap)
            yield next_term
            seen.remove(next_term)
        for a in range(1, k//2+1):
            x = a**2+(k-a)**2
            if x not in seen:
                seen.add(x)
                heappush(heap, x)

oeis_groundtruth = [    2, 5, 8, 10, 13, 17, 18, 20, 25, 26, 29, 32, 34, 37, 40, 41, 45, 50, 52, 53, 58, 61, 65, 68, 72, 73, 74, 80, 82, 85, 89, 90, 97, 98, 100, 101, 104, 106, 109, 113, 116, 117, 122, 125, 128, 130, 136, 137, 145, 146, 148, 149, 153, 157, 160, 162, 164, 169, 170, 173, 178]

result = list(islice(sums_of_two_nonzero_squares(), len(oeis_groundtruth)))

print(result)
# [2, 5, 8, 10, 13, 17, 18, 20, 25, 26, 29, 32, 34, 37, 40, 41, 45, 50, 52, 53, 58, 61, 65, 68, 72, 73, 74, 80, 82, 85, 89, 90, 97, 98, 100, 101, 104, 106, 109, 113, 116, 117, 122, 125, 128, 130, 136, 137, 145, 146, 148, 149, 153, 157, 160, 162, 164, 169, 170, 173, 178]

print('result == oeis_groundtruth: ', (result == oeis_groundtruth))
# result == oeis_groundtruth:  True

评论

1赞 btilly 12/14/2022
这将是我的第一种方法。但请注意,每个堆操作都是 .这使得这不仅仅是线性时间。增长速度也比使空间比您想象的要差略快。O(log(k))ksqrt(n)
0赞 Stef 12/14/2022
@btilly你对如何找到 K 增长的边界有一些见解?
0赞 btilly 12/15/2022
我可以放置一个上限。我无法证明更严格的限制,但我认为真正的限制是.我会在我的回答中加入这背后的理论。k = O(sqrt(n log(n)))k = O(sqrt(n log(log(n))))
0赞 btilly 12/15/2022
理论添加到我回答的末尾。
0赞 Stef 12/15/2022
@btilly 好吧,如果 k = O(sqrt(n log(n))),那么这个算法使用 O(k²) = O(n log(n)) 空间,并且 O(k² log(k)) = O(n log(n)²) 时间。这几乎和 O(n) 一样好。
5赞 user1196549 12/13/2022 #2

我不知道你怎么称呼蛮力,但这个问题可能可以通过详尽的搜索在时间O(n)内解决。

考虑一个边 2√n 的正方形,原点处有一个角,计算该正方形内的所有值,并每次在位数组中设置一个位。

这需要与 (2√n)² = 4n 成比例的运算。


要使这种方法起作用,必须检查 2√n 是否足够大以生成 n 个第一个毕达哥拉斯和。

评论

0赞 btilly 12/14/2022
事实证明,2 sqrt(n) 还不够大。
0赞 12/14/2022
@btilly:你知道安全绑定吗?
0赞 btilly 12/14/2022
所有素数都是 2 个平方的总和。这些密度是.所以对于足够大的,将是一个上限。我敢打赌,有一个更好的,就像.但我会给出一个答案,展示如何在没有太多工作的情况下懒洋洋地发现它,也没有疯狂的内存需求。1 mod 41/(2 log(n))n2 n log(n)O(n log(log(n))))
1赞 btilly 12/14/2022 #3

这是 https://stackoverflow.com/a/74787074/585411 的变体。

如果它作为答案返回,那么它确实有效并且需要内存。正如我对 @yves-daoust 评论的那样,不是,但可以证明是并且可能是类似的东西。所以这比二次要好得多。mO(m)O(sqrt(m) log(m))mO(n)O(n log(n))O(n log(log(n))))

import math

def nth_two_square(n):
    squares = [1, 4]
    high = 1

    while True:
        low = high + 1
        high = squares[-1]+1
        block = [False] * (high - low + 1)
        i = len(squares) - 1
        j = 0
        while squares[j] + squares[j] <= high:
            while high < squares[j] + squares[i]:
                i -= 1
            k = i
            while j <= k and low <= squares[j] + squares[k]:
                block[squares[j] + squares[k] - low] = True
                k = k-1
            j = j + 1

        for i in range(len(block)):
            if block[i]:
                n -= 1
                if n < 1:
                    return low + i

        to_add = int(math.log(squares[-1]))
        for _ in range(to_add):
            squares.append((len(squares)+1)*(len(squares)+1))

print(nth_two_square(1000000))

如果不是检查我们返回哪个,我们将得到一个可以按顺序返回序列的版本。n

def two_squares():
    squares = [1, 4]
    high = 1

    while True:
        low = high + 1
        high = squares[-1]+1
        block = [False] * (high - low + 1)
        i = len(squares) - 1
        j = 0
        while squares[j] + squares[j] <= high:
            while high < squares[j] + squares[i]:
                i -= 1
            k = i
            while j <= k and low <= squares[j] + squares[k]:
                block[squares[j] + squares[k] - low] = True
                k = k-1
            j = j + 1

        for i in range(len(block)):
            if block[i]:
                yield low + i

        to_add = int(math.log(squares[-1]))
        for _ in range(to_add):
            squares.append((len(squares)+1)*(len(squares)+1))

n = 100
for m in two_squares():
    print(m)
    n -= 1
    if n < 1:
        break

这背后的一些理论解释了我对其他答案留下的评论。

形式为 with 和 整数的复数集合称为高斯整数。高斯整数的行为很像整数,它们对素数具有独特的因式分解,等等。然而,高斯素数略有不同。a + biab

首先,素数以四组为一组。如果两个数相同,则认为它们是相同的素数,直到 、 或 的因数。1i-1-i

接下来,如果一个数字是两个平方的总和,它就不能是高斯素数。那是因为.相反,如果我们能将一个数字分解为高斯素数,那么我们就可以列出它的所有除数。它是两个平方的总和,当且仅当我们能找到形式的除数时。(a+bi)(a-bi) = a^2+b^2a+bi

所以这意味着这不是高斯素数。同上也不是。但 IS 是高斯素数。1^2+1^2 = 22 = (1+i)(1-i)5 = (1+4i)(1-4i)7

一般结果来自费马的结果。整数中的素数是高斯素数,当且仅当它除以 3 时余数为 1。所以 、 和 都是高斯素数。(进一步的结果是,所有不是高斯素数的素数都可以用一种且只能一种方式写成平方和。71119251317

现在假设除以一些次。然后有这样的.但是,它必须至少同样频繁地分裂。颠倒论证中的符号,我们还发现除法的频率至少与除法的频率一样多。因此,它分裂和相同的次数。7a+bic+di(c+di)*7^k = a+bi(c-di)*7^k = a-bia-bi7a+bia-bia+bia-bi

由此我们发现,它总是被数次除以。同样的论点也适用于所有其他也是高斯素数的素数。(这是我开始挥手的地方,但你可以相信这个论点是可以形成的。大约一半的素数与 全等,典型的大整数可以被其中至少一个奇数次整除。因此,对于大数,作为 2 个平方和的数字的密度会下降。7a^2 + b^23 mod 4

但它只会慢慢下降。素数的密度只像 一样下降,其中一半与 一致。因此,密度下降的速度不能比 快。1/log(n)1 mod 41/(2 log(n))

而且,当然,大多数 2 平方的总和都不是质数。所以它下降的速度比这慢。不过,我对此没有很好的估计,只是一个有根据的猜测。