最长的 Collat​​z(或 Hailstone)序列优化 - Python 2.7

Longest Collatz (or Hailstone) sequence optimization - Python 2.7

我制作了一个程序,打印出一个数字列表,每个数字的步数(根据 Collatz Conjecture)达到 1 所需的步数比前一个多:

limit = 1000000000
maximum = 0
known = {}
for num in xrange(2, limit):
    start_num = num
    steps = 0
    while num != 1:
        if num < start_num:
            steps += known[num]
            break;
        if num & 1:
            num = (num*3)+1
            steps += 1
        steps += 1
        num //= 2
    known[start_num] = steps
    if steps > maximum:
        print start_num,"\t",steps
        maximum = steps

我缓存我已经知道的结果来加速程序。此方法工作到 10 亿的限制,我的计算机内存不足 (8GB)。

  1. 是否有更有效的方法来缓存结果?
  2. 有没有办法进一步优化这个程序?

提前致谢。

似乎天生就很难极大地加速 Collat​​z 程序;我所知道的最好的程序 are distributed,在全球数百(数千 ...)台 PC 上使用空闲周期。

在纯 CPython 中,您可以做一些简单的事情来优化您的程序,尽管速度和 space 优化通常是不一致的:

  • Speed:Python 中的计算量大的程序应始终编写为函数,而不是主程序。那是因为局部变量访问明显快于全局变量访问。
  • Space:使 known 成为一个列表而不是一个字典需要更少的内存。您正在为每个数字存储一些东西;字典更适合稀疏映射。
  • Spacearray.array 仍然需要更少的 space - 虽然比使用列表慢。
  • 速度:对于奇数n3*n + 1必然是偶数,所以你可以通过转到[=15将2步折叠成1步=]直接。
  • 速度:给定最终结果(数字和步数),您可以跳到前面并填写该数字乘以 2 的所有次方的结果。例如,如果n 走了 s 步,然后 2*n 将走 s+14*n 将走 s+28*n 将走 [=23] =],等等。

尽管我使用的是 Python 3(在 Python 2 中,您至少要将 range 更改为 [=25,但这是包含所有这些建议的一些代码=]).请注意,启动时有很长的延迟 - 这是用十亿个 32 位无符号零填充大 array 所花费的时间。

def coll(limit):
    from array import array
    maximum = 0
    known = array("L", (0 for i in range(limit)))
    for num in range(2, limit):
        steps = known[num]
        if steps:
            if steps > maximum:
                print(num, "\t", steps)
                maximum = steps
        else:
            start_num = num
            steps = 0
            while num != 1:
                if num < start_num:
                    steps += known[num]
                    break
                while num & 1:
                    num += (num >> 1) + 1
                    steps += 2
                while num & 1 == 0:
                    num >>= 1
                    steps += 1
            if steps > maximum:
                print(start_num, "\t", steps)
                maximum = steps
            while start_num < limit:
                assert known[start_num] == 0
                known[start_num] = steps
                start_num <<= 1
                steps += 1

coll(1000000000)

获得奇闻趣事

1992 年撰写的一份技术报告提供了多种加速此类搜索的方法:"3x+1 Search Programs", by Leavens and Vermeulen。例如,@Jim Mischel 的 "cut off based on previous peaks" 想法本质上是论文的引理 20.

另一个:对于简单的因子 2,请注意您可以 "almost always" 忽略甚至起始数字。为什么:让 s(n) 表示达到 1 所需的步数。您正在寻找 s() 值中的新峰值。假设在 n 处发现了最近的峰值,并且您正在考虑 in < i < 2*n 的偶数整数。然后特别是 i/2 < n,所以 s(i/2) < s(n)(根据 "peak" 的定义,在 n 处达到了新的峰值)。但是s(i) == s(i/2) + 1,所以s(i) <= s(n)i不可能是新的高峰

因此在 n 处找到新峰后,您可以跳过所有偶数(但不包括)2*n

论文中还有许多其他有用的想法 - 但它们并不都是 容易的 ;-)

你真的只需要缓存奇数。在你的程序中考虑当你开始处理一个数字时会发生什么。

如果您使用起始编号 X 并执行 mod 4,您最终会遇到以下四种情况之一:

  • 0 或 2:重复除以 2 最终会得到一个小于 X 的奇数。您已缓存该值。所以你可以只计算除以 2 的次数,将其添加到缓存值,你就得到了序列长度。
  • 1: (3x+1)/2 会得到一个偶数,再除以 2 会得到一个小于 X 的数。如果结果是奇数,那么你已经有了缓存值, 所以你可以只加 3。如果结果是偶数,重复除以2,直到得到一个奇数(你已经缓存了),将缓存的值加上3和除以2的次数,就得到了序列长度[=47] =]
  • 3:进行标准的 Collat​​z 序列计算,直到得到一个小于起始数字的数字。然后你要么缓存了这个值,要么这个数字是偶数,你重复除以 2 直到你得到一个奇数。

这可能会稍微减慢您的程序,因为您有更多的 2 除数,但它会使您的缓存容量翻倍。

您可以通过仅保存 x mod 4 == 3 处数字的序列长度来再次将缓存容量加倍,但代价是更多的处理时间。

那些只会让您线性增加缓存 space。您真正需要的是一种整理缓存的方法,这样您就不必保存那么多结果。以一些处理时间为代价,您只需要缓存产生迄今为止找到的最长序列的数字。

考虑一下,当您计算 27 有 111 步时,您已经节省了:

starting value, steps
1, 0
2, 1
3, 7
6, 8
7, 16
9, 19
18, 20
25, 23
27, 111

所以当你看到28时,你除以2得到14。搜索你的缓存,你看到14到1的步数不能超过19(因为没有数字小于18需要超过 19 个步骤)。所以最大可能的序列长度是20。但是你已经有最大的111。所以你可以停止。

这可能会花费您更多的处理时间,但会大大扩展您的缓存。您只有 44 个条目,一直到 837799。请参阅 https://oeis.org/A006877

有趣的是,如果您对这些数字绘制对数散点图,您会得到非常接近直线的近似值。参见 https://oeis.org/A006877/graph

您可以通过保留第二个缓存来组合方法,该缓存告诉您,对于大于当前最大值的数字,需要多少步才能使该数字低于当前最大值。所以在上面的例子中,27 是当前的最大值,你会为数字 35 存储 26,因为它需要六次操作(106、53、160、80、40、20)才能得到 35 到 20。 table 告诉您最多需要 20 步才能达到 1,因此最多可能有 26 步。因此,如果任何其他值减少到 35,则将当前步数添加到 26,如果该数字小于 111,则您知道您不可能使用此数字获得新的最大值。如果大于111,则需要继续计算整个数列。

每当找到新的最大值时,将生成它的数字添加到第一个缓存,并清除第二个缓存。

这比缓存每个值的结果要慢(我的直觉是在最坏的情况下它可能会加倍你的处理时间),但它会大大扩展你的范围。

这里的关键是扩大范围将以牺牲一些速度为代价。这是一个常见的权衡。正如我在上面指出的,您可以做很多事情来保存每第 n 个项目,这将为您提供更大的缓存。所以如果你每 4 个值保存一次,你的缓存实际上是你保存每个值的 4 倍。但是你很快就达到了 returns 减少的程度。也就是说,比原始缓存大 10 倍的缓存并不比 9 倍缓存大很多。

我的建议实质上是以一些处理时间为代价,使缓存呈指数级增长 space。但这不应该是处理时间的巨大增加,因为在最坏的情况下,下一个最大值的数字将是前一个最大值的两倍。 (想想 27,有 111 步,54,有 112 步。)它需要更多的代码来维护,但它应该扩展你的范围,目前只有 30 位,远远超过 40 位。