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

时间:2016-08-11 00:40:51

标签: python algorithm caching optimization collatz

我制作了一个程序,打印出一个数字列表,每个数字都需要比前一个步骤更多的步骤(根据Collatz Conjecture):

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. 有没有办法进一步优化这个程序?
  3. 提前谢谢。

2 个答案:

答案 0 :(得分:6)

看起来很难加速Collat​​z计划;我知道are distributed的最佳程序,使用全球数百(数千......)台PC上的空闲周期。

在纯CPython中,您可以采取一些简单的方法来优化您的程序,尽管速度和空间优化通常不一致:

  • 速度:Python中的计算量很大的程序应始终作为函数编写,而不是作为主程序编写。这是因为局部变量访问明显快于全局变量访问。
  • 空间:使known列表而不是字典需要更少的内存。你为每个号码存储一些东西; dicts更适合稀疏映射。
  • 空间array.array需要更少的空间 - 尽管比使用列表要慢。
  • 速度:对于奇数n3*n + 1必须是偶数,因此您可以直接转到(3*n + 1)//2 == n + (n >> 1) + 1将2个步骤合并为1。 / LI>
  • 速度:给定最终结果(数字和步数),您可以向前跳并填写该数字的结果乘以2的所有幂。例如,如果n花了s步,然后2*ns+14*ns+28*ns+3,所以上。

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

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)

获得GONZO

1992年撰写的一份技术报告提供了许多加速此类搜索的方法:"3x+1 Search Programs", by Leavens and Vermeulen。例如,@ Jim Mischel&#34;根据以前的峰值切断了#34;这个想法基本上就是论文的引理20。

另一个:对于2的简单因子,请注意,您几乎总是可以&#34;甚至忽略起始号码。为什么:让s(n)表示达到1所需的步骤数。您正在寻找s()值的新峰值。假设在n找到了最近的峰值,并且您正在考虑使用i的偶数整数n < i < 2*n。然后特别是i/2 < n,所以s(i/2) < s(n)(根据&#34的定义;峰值&#34;以及在n达到新的峰值)。但s(i) == s(i/2) + 1s(i) <= s(n)i不能成为新的高峰。

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

本文还有许多其他有用的想法 - 但它们并非 容易; - )

答案 1 :(得分:4)

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

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

  • 0或2:重复除以2将最终导致奇数小于X.您已缓存该值。因此,您可以将除数除以2,将其添加到缓存值,并且您具有序列长度。
  • 1:(3x + 1)/ 2将产生偶数,再将其除以2将导致数字小于X.如果结果为奇数,那么您已经拥有缓存值,因此您只需添加3即可。如果结果是偶数,则重复除以2,直到得到一个奇数(已经缓存),将3和除以2的除法数加到缓存值,然后得到序列长度。
  • 3:执行标准的Collat​​z序列计算,直到得到小于起始编号的数字。然后你要么缓存值,要么数字是偶数,你重复除以2,直到你得到一个奇数。

这可能会使你的程序稍微减慢一点,因为你有两个除以2的分数,但它会使你的缓存容量增加一倍。

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

那些只能让你在缓存空间中线性增加。你真正需要的是一种修饰缓存的方法,这样你就不必保存那么多结果。以一些处理时间为代价,您只需要缓存产生迄今为止发现的最长序列的数字。

考虑到当您计算出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,因为它需要6次操作(106,53,160,80,40,20)才能获得35到20。表告诉您,它不能超过20个步骤才能达到1,最多可能有26个步骤。因此,如果任何其他值减少到35,则将当前步数添加到26,如果该数字小于111,则表示您可能无法使用此数字获得新的最大值。如果数字大于111,则必须继续计算整个序列。

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

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

这里的关键是扩展你的射程将以某种速度为代价。这是一种常见的权衡。正如我在上面所指出的,你可以做很多事情来保存每个第n项,这将为你提供一个更大的缓存。因此,如果保存每个第4个值,则缓存基本上是保存每个值的4倍。但是你很快就会达到收益递减的程度。也就是说,比原始缓存大10倍的缓存并不比9倍缓存大很多。

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