【问题标题】:Efficiently generating Stern's Diatomic Sequence高效生成 Stern 的双原子序列
【发布时间】:2017-05-04 21:17:24
【问题描述】:

斯特恩的双原子序列可以阅读更多细节over here;但是,出于我的目的,我现在将定义它。


斯特恩双原子序列的定义

n 是一个数字以生成fusc 函数。记为fusc(n)

如果n 为0,则返回值为0。
如果n 为 1,则返回值为 1。

如果n 是偶数,则返回值为fusc(n / 2)
如果n 为奇数,则返回值为fusc((n - 1) / 2) + fusc((n + 1) / 2)


目前,我的 Python 代码在生成的大部分时间里都是蛮力的,除了除以两部分,因为它总是不会产生任何变化。

def fusc (n):
    if n <= 1:
        return n

    while n > 2 and n % 2 == 0:
        n /= 2

    return fusc((n - 1) / 2) + fusc((n + 1) / 2)

但是,我的代码必须能够处理 1000s 百万位数量级的数字,并且递归地运行函数 数千次 百万次似乎不是很高效或实用。

有什么方法可以在算法上改进我的代码,这样就可以传递大量数字而不必递归调用函数这么多次?

【问题讨论】:

  • 阅读有关 memoization 的信息:en.wikipedia.org/wiki/Memoization 使用 python 可以很好地完成,但现在我不确定这是否是一个好的解决方案,因为你不会计算太多次相同的事情。 ..
  • 记忆化可能会在减少到较小的数字时略有帮助,但我需要能够快速计算出大量的数字,尽管实现这样的东西肯定会提高长期性能,但事实并非如此真的是我想要的。
  • 这就是为什么它变成了评论而不是答案。祝你好运。

标签: python algorithm performance python-2.7


【解决方案1】:

使用一百万位的记忆,递归堆栈将非常大。我们可以首先尝试查看一个足够大的数字,我们可以手动处理,在这种情况下为fusc(71)

fusc(71) = fusc(35) + fusc(36)

fusc(35) = fusc(17) + fusc(18)
fusc(36) = fusc(18)

fusc(71) = 1 * fusc(17) + 2 * fusc(18)

fusc(17) = fusc(8) + fusc(9)
fusc(18) = fusc(9)

fusc(71) = 1 * fusc(8) + 3 * fusc(9)

fusc(8) = fusc(4)
fusc(9) = fusc(4) + fusc(5)

fusc(71) = 4 * fusc(4) + 3 * fusc(5)

fusc(4) = fusc(2)
fusc(3) = fusc(1) + fusc(2)

fusc(71) = 7 * fusc(2) + 3 * fusc(3)

fusc(2) = fusc(1)
fusc(3) = fusc(1) + fusc(2)

fusc(71) = 11 * fusc(1) + 3 * fusc(2)

fusc(2) = fusc(1)

fusc(71) = 14 * fusc(1) = 14

我们意识到在这种情况下我们可以完全避免递归,因为我们总是可以将fusc(n) 表达为a * fusc(m) + b * fusc(m+1),同时将m 的值减小为0。从上面的示例中,您可能会发现以下模式:

如果 m 是奇数:
a * fusc(m) + b * fusc(m+1) = a * fusc((m-1)/2) + (b+a) * fusc((m+1)/2)
如果 m 是偶数:
a * fusc(m) + b * fusc(m+1) = (a+b) * fusc(m/2) + b * fusc((m/2)+1)

因此,您可以使用简单的循环函数在 O(lg(n)) 时间内解决问题

def fusc(n):
  if n == 0: return 0
  a = 1
  b = 0
  while n > 0:
    if n%2:
      b = b + a
      n = (n-1)/2
    else:
      a = a + b
      n = n/2
  return b

【讨论】:

    【解决方案2】:

    lru_cache 在你的情况下可以创造奇迹。确保 maxsize 是 2 的幂。可能需要为您的应用程序调整该大小。 cache_info() 将对此有所帮助。

    也使用// 代替/ 进行整数除法。

    from functools import lru_cache
    
    
    @lru_cache(maxsize=512, typed=False)
    def fusc(n):
    
        if n <= 1:
            return n
    
        while n > 2 and n % 2 == 0:
            n //= 2
    
        return fusc((n - 1) // 2) + fusc((n + 1) // 2)
    
    print(fusc(1000000000078093254329870980000043298))
    print(fusc.cache_info())
    

    是的,这只是 Filip Malczak 提出的 meomization。

    您可能会在 while 循环中使用位操作获得额外的 tiny 加速:

    while not n & 1:  # as long as the lowest bit is not 1
        n >>= 1       # shift n right by one
    

    更新

    这是一种“手动”进行 meomzation 的简单方法:

    def fusc(n, _mem={}):  # _mem will be the cache of the values 
                           # that have been calculated before
        if n in _mem:      # if we know that one: just return the value
            return _mem[n]
    
        if n <= 1:
            return n
    
        while not n & 1:
            n >>= 1
        if n == 1:
            return 1    
    
        ret = fusc((n - 1) // 2) + fusc((n + 1) // 2)
        _mem[n] = ret  # store the value for next time
        return ret
    

    更新

    在阅读了short article by dijkstra himself 之后的小更新。

    文章指出,f(n) = f(m) 如果m 的第一个和最后一个位与n 的相同并且它们之间的位被反转。我们的想法是让n 尽可能小。

    这就是位掩码(1&lt;&lt;n.bit_length()-1)-2 的用途(第一个和最后一个位是0;中间的那些是1xoring n 与上面描述的那样给出m)。

    我只能做小的基准测试;如果这对您的输入量有任何帮助,我很感兴趣...这将减少缓存的内存并希望带来一些加速。

    def fusc_ed(n, _mem={}):
    
        if n <= 1:
            return n
    
        while not n & 1:
            n >>= 1
        if n == 1:
            return 1
    
        # https://www.cs.utexas.edu/users/EWD/transcriptions/EWD05xx/EWD578.html
        # bit invert the middle bits and check if this is smaller than n
        m = n ^ (1<<n.bit_length()-1)-2
        n = m if m < n else n
    
        if n in _mem:
            return _mem[n]
    
        ret = fusc(n >> 1) + fusc((n >> 1) + 1)
        _mem[n] = ret
        return ret
    

    我不得不增加递归限制:

    import sys
    sys.setrecursionlimit(10000)  # default limit was 1000
    

    基准测试给出了奇怪的结果;使用下面的代码并确保我总是开始一个新的interperter(有一个空的_mem)我有时会得到更好的运行时;在其他情况下,新代码速度较慢...

    基准代码:

    print(n.bit_length())
    
    ti = timeit('fusc(n)', setup='from __main__ import fusc, n', number=1)
    print(ti)
    
    ti = timeit('fusc_ed(n)', setup='from __main__ import fusc_ed, n', number=1)
    print(ti)
    

    这是我得到的三个随机结果:

    6959
    24.117448464001427
    0.013900151001507766
    
    6989
    23.92404893300045
    0.013844672999766772
    
    7038
    24.33894686200074
    24.685758719999285
    

    那是我停下来的地方......

    【讨论】:

    • 不幸的是,我使用的是 Python2.7,lru_cache 不可用。我应该在标签中添加 Python2.7。
    • stackoverflow.com/a/11861795/4954037 可能会有所帮助......或者你可以自己动手。
    • 也不能使用任何外部库。我认为解决方案需要几乎完全是算法。
    • 添加了一个没有使用lru_cache的meomized版本。请在放弃缓存值的想法之前尝试一下。加速是巨大的!
    • 我认为stackoverflow.com/a/44557597/1400793 应该是公认的答案。另一个答案中的代码不包含递归,显然是 O(log n),而且非常简单。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-24
    • 2016-01-03
    • 1970-01-01
    相关资源
    最近更新 更多