【问题标题】:New state of the art in unlimited generation of Hamming sequence无限生成汉明序列的最新技术
【发布时间】:2022-01-12 12:08:52
【问题描述】:

(这很令人兴奋!)我知道,主题众所周知。最先进的技术(在 Haskell 以及其他语言中)有效地生成无限递增的汉明数序列,没有重复和没有遗漏,长期以来一直如下(AFAIK - 顺便说一句,它也等同于original Edsger Dijkstra's code ):

hamm :: [Integer]
hamm = 1 : map (2*) hamm `union` map (3*) hamm `union` map (5*) hamm
  where
    union a@(x:xs) b@(y:ys) = case compare x y of
        LT -> x : union  xs  b
        EQ -> x : union  xs  ys
        GT -> y : union  a   ys

我要问的问题是,你能找到提高效率的方法吗?它仍然是最先进的,还是实际上有可能改进它以使运行速度快两倍并以更好的empirical orders of growth 启动?

如果您的回答是,请显示代码并讨论与上述相比的速度和经验增长顺序(对于前几十万个数字,它的运行速度约为 ~ n^1.05 .. n^1.10产生)。另外,如果存在的话,这种高效的算法是否可以扩展到生成具有任何给定素数集的平滑数序列?

【问题讨论】:

  • 1.我本来期望像 O(n log n) 这样的分析,你确定这和你建议的多项式一样糟糕吗? 2. 无论语言如何,这不是最先进的吗?
  • @DanielWagner 1. 这是经验数据,请查看 WP 链接(~ n log n 通常表现为n^(1+a)a 较低)2. 是 问题.... :)
  • @DanielWagner 关于你的 1.,n^(1+a) 中的 a 为真 ~ n log n 应该随着 n 的增长而减少,但这里内存保留开始发挥作用,然后 bignum 算术开始付出代价;所以在实践中,经典代码的a 会增长,n = 100,000 ... 1 百万及以上。
  • 还有,理论上这是O(n)算法。
  • 相关:smooth-numbers

标签: haskell recursion generator primes hamming-numbers


【解决方案1】:

如果一个常数因素(1) 的加速很重要,那么我可以提供一个更高效的版本:

hamm :: [Integer]
hamm = mrg1 hamm3 (map (2*) hamm)
  where
    hamm5 = iterate (5*) 1
    hamm3 = mrg1 hamm5 (map (3*) hamm3)
    merge a@(x:xs) b@(y:ys)
        | x < y     = x : merge xs b
        | otherwise = y : merge a ys
    mrg1 (x:xs) ys = x : merge xs ys

您可以轻松地将其推广到平滑给定素数集的数字:

hamm :: [Integer] -> [Integer]
hamm [] = [1]
hamm [p] = iterate (p*) 1
hamm ps = foldl' next (iterate (q*) 1) qs
  where
    (q:qs) = sortBy (flip compare) ps
    next prev m = let res = mrg1 prev (map (m*) res) in res
    merge a@(x:xs) b@(y:ys)
        | x < y     = x : merge xs b
        | otherwise = y : merge a ys
    mrg1 (x:xs) ys = x : merge xs ys

它更高效,因为该算法不会产生任何重复并且它使用更少的内存。在您的版本中,当产生接近h 的汉明数时,h/5h 之间的列表部分必须在内存中。在我的版本中,只有完整列表的h/2h 之间的部分,以及3-5-list 的h/3h 之间的部分需要在内存中。由于 3-5-list 更稀疏,并且 k-smooth 数的密度降低,这两个列表部分需要的内存比完整列表的较大部分要少得多。

两种算法产生kth汉明数的一些时间,每个目标相对于前一个目标的经验复杂度,不包括和包括GC时间:

  k            Yours (MUT/GC)               Mine (MUT/GC)
 10^5           0.03/0.01                    0.01/0.01      -- too short to say much, really
2*10^5          0.07/0.02                    0.02/0.01
5*10^5          0.17/0.06  0.968  1.024      0.06/0.04      1.199    1.314
 10^6           0.36/0.13  1.082  1.091      0.11/0.10      0.874    1.070
2*10^6          0.77/0.27  1.097  1.086      0.21/0.21      0.933    1.000
5*10^6          1.96/0.71  1.020  1.029      0.55/0.59      1.051    1.090
 10^7           4.05/1.45  1.047  1.043      1.14/1.25      1.052    1.068
2*10^7          8.73/2.99  1.108  1.091      2.31/2.65      1.019    1.053
5*10^7         21.53/7.83  0.985  1.002      6.01/7.05      1.044    1.057
 10^8          45.83/16.79 1.090  1.093     12.42/15.26     1.047    1.084

如你所见,MUT时间之间的因子约为3.5,但GC时间相差不大。

(1) 嗯,它看起来是恒定的,而且我认为两种变体具有相同的计算复杂度,但我没有拿出纸笔来证明这一点,我也不打算这样做。

【讨论】:

  • 是的,你成功了。稍后我将发布更多详细信息的答案。 功劳不归我;我只是在某个地方偶然发现了它,并想让它为人所知。请注意,在较低范围内,您的加速不是 3.6,而是 3.3 和 2.8。我现在所拥有的,在低范围内提供了 2.5 的加速。但你似乎成功地重新发明了这个轮子,并让它变得更快。我认为这是一个令人振奋的消息。 :)
  • 在实验错误中,fix 版本和我的时间相同。我的分配了大约 200 字节,并且驻留率略高,但数字非常接近(难怪,它基本上是相同的算法)。
【解决方案2】:

所以基本上,既然 Daniel Fischer 给出了他的答案,我可以说我最近遇到了这个问题,我认为这是一个令人兴奋的发展,因为自 Dijkstra 以来,经典代码已经为人所知很久了。

Daniel 正确识别出重复生成的冗余,然后在经典版本中必须将其删除。

The credit for the original discovery (AFAIK) goes to Rosettacode.org's contributor Ledrug,截至 2012 年 8 月 26 日。当然还有 the independent discovery by Daniel Fischer, here (2012-09-18)。

稍微改写一下,代码是:

import Data.Function (fix)

hamm = 1 : foldr (\n s -> fix (merge s . (n:) . map (n*))) [] [2,3,5]

与通常的合并实现,

merge a@(x:xs) b@(y:ys) | x < y     = x : merge xs b
                        | otherwise = y : merge a ys
merge [] b = b
merge a [] = a

与经典版本相比,它提供了大约 2.0 到 2.5 倍的加速。

【讨论】:

    【解决方案3】:

    嗯,这比我想象的要容易。这将在我家里的慢速 PC 上在 0.05 秒内完成 1000 次 Hammings。今天下午在工作中,不到 600 次的更快 PC 时间以零秒出现。

    这从 Hammings 那里得到了 Hammings。它基于在 Excel 中执行速度最快。

    我在 250000 之后得到了错误的数字,Int。数字增长得非常快非常快,所以必须使用Integer,因为Int 是有界的。

    mkHamm :: [Integer] -> [Integer] -> [Integer] -> [Integer] 
           -> Int -> (Integer, [Int])
    mkHamm ml (x:xs) (y:ys) (z:zs) n =  
       if n <= 1
           then (last ml, map length [(x:xs), (y:ys), (z:zs)])
           else mkHamm (ml++[m]) as bs cs (n-1)
         where
             m = minimum [x,y,z]
             as = if x == m then xs ++ [m*2] else (x:xs) ++ [m*2]
             bs = if y == m then ys ++ [m*3] else (y:ys) ++ [m*3]
             cs = if z == m then zs ++ [m*5] else (z:zs) ++ [m*5]
    

    测试,

    > mkHamm  [1] [2] [3] [5] 5000
    (50837316566580,[306,479,692])        -- (0.41 secs)
    
    > mkHamm  [1] [2] [3] [5] 10000
    (288325195312500000,[488,767,1109])   -- (1.79 secs)
    
    > logBase 2 (1.79/0.41)     -- log of times ratio = 
    2.1262637726461726          --   empirical order of growth
    
    > map (logBase 2) [488/306, 767/479, 1109/692] :: [Float]
    [0.6733495, 0.6792009, 0.68041545]     -- leftovers sizes ratios
    

    这意味着此代码的运行时间 empirical order of growth 高于二次(~n^2.13 在 GHCi 提示符下测量、解释)。

    此外,序列中三个悬垂的过度生产片段的大小各为 ~n^0.67,即 ~n^(2/3)

    此外,这段代码是非惰性的:结果序列的第一个元素只能在最后一个元素被计算后才能访问。

    问题中最先进的代码是线性的,在兴趣点之后正好过量生成 0 个元素,并且非常懒惰:它立即开始生成数字。

    因此,尽管这张海报对之前的答案有很大的改进,但它仍然比原来的答案差很多,更不用说它出现在前两个答案中的改进了。

    12.31.2018

    只有最优秀的人才接受教育。 @Will Ness 还在 GoalKicker.com “Haskell for Professionals”中撰写或共同撰写了 19 个章节。免费的书是宝藏。

    我已经提出了一个可以做到这一点的函数的想法,就像这样。我很担心,因为我认为它会像某些现代语言一样复杂且涉及逻辑。我决定开始写作,并惊讶于 Haskell 可以轻松实现甚至是糟糕的想法。

    生成唯一列表没有任何困难。我的问题是我生成的列表不能很好地结束。即使当我使用对角化时,它们也会留下剩余值,从而使它们的使用充其量是不可靠的。

    这是一个重新设计的 3 和 5 列表,最后没有任何残留。去国有化是为了减少剩余价值,而不是为了消除无论如何都不会包含的重复项。

    g3s5s n=[t*b|(a,b)<-[ (((d+n)-(d*2)), 5^d) | d <- [0..n]],
                    t <-[ 3^e | e <- [0..a+8]],
                 (t*b)<-(3^(n+6))+a]                                       
    
    
    ham2 n = take n $ ham2' (drop 1.sort.g3s5s $ 48) [1]
    
    ham2' o@(f:fo) e@(h:hx) = if h == min h f
                          then h:ham2'  o (hx ++ [h*2])
                          else f:ham2' fo ( e ++ [f*2])
    

    twos 列表可以通过所有2^es 乘以每个3s5s 来生成,但是当包括身份2^0 时,总共是Hammings。

    2019 年 3 月 25 日

    好吧,最后。我前段时间知道这一点,但最终如果没有多余的价值就无法实施。问题是如何不产生笛卡尔积导致的多余部分。我经常使用 Excel,但看不到要从笛卡尔积工作表中排除的值模式。然后,尤里卡!这些函数生成每个领先因素的列表。限制每个列表中的值的值是第一个列表的终点。完成此操作后,所有的 Hammings 都会生成,没有多余的。

    Hammings 的两个函数。第一个是新的 3 和 5 列表,然后用于创建与 2 的倍数。倍数是 Hammings。

    h35r  x = h3s5s x (5^x)
    h3s5s x c = [t| n<-[3^e|e<-[0..x]],
                    m<-[5^e|e<-[0..x]], 
                    t<-[n*m],
                 t <= c ]
    
    a2r n = sort $ a2s n (2^n)
    a2s n c =   [h| b<-h35r n, 
                    a<-[2^e| e<-[0..n]],
                    h<-[a*b],
                 h <= c ] 
    

    last $ a2r 50

    1125899906842624

    (0.16 秒,321,326,648 字节)

    2^50

    1125899906842624

    (0.00 秒,95,424 字节

    这是一种替代的、更干净、更快且内存使用量更少的实现。

    gnf n f = scanl (*) 1 $ replicate f n
    
    mk35   n = (\c->      [m| t<- gnf 3 n, f<- gnf 5  n,    m<- [t*f], m<= c]) (2^(n+1))
    mkHams n = (\c-> sort [m| t<- mk35  n, f<- gnf 2 (n+1), m<- [t*f], m<= c]) (2^(n+1))
    

    last $ mkHams 50

    2251799813685248

    (0.03 秒,12,869,000 字节)

    2^51

    2251799813685248

    2019 年 5 月 6 日

    嗯,我尝试过不同的限制,但总是回到最简单的。我选择了最少的内存使用,因为它似乎也是最快的。

    我还选择使用带有隐式参数的map

    我还发现Data.List.Ordered 中的mergeAllsortsortconcat 更快。

    我也喜欢创建子列表,这样我可以更轻松地分析数据。

    然后,因为@Will Ness 切换到iterate 而不是scanl,所以代码更简洁。也因为@Will Ness,我停止使用最后一个 2s 列表并切换到一个确定所有长度的值。

    我确实认为递归定义的列表更有效,前一个数字乘以一个因子。

    只是将函数分成两个并没有什么区别,所以 3 和 5 的倍数是

    m35 lim = mergeAll $ 
              map (takeWhile (<=lim).iterate (*3)) $
                   takeWhile (<=lim).iterate (*5)  $ 1
    

    2s 分别乘以 3s 和 5s 的乘积

    ham n = mergeAll $
            map (takeWhile (<=lim).iterate (*2)) $ m35 lim 
        where lim= 2^n
    

    编辑函数后我运行它

    last $ ham 50

    1125899906842624

    (0.00 秒,7,029,728 字节)

    然后

    last $ ham 100

    1267650600228229401496703205376

    (0.03 秒,64,395,928 字节)

    使用10^n 可能更好,但为了比较,我再次使用了2^n

    2019 年 5 月 11 日

    因为我更喜欢无限和递归列表,所以我有点痴迷于使这些无限。

    @Daniel Wagner 和他的Data.Universe.Helpers 给我留下了深刻的印象和启发,我开始使用+*++++,但后来添加了我自己的无限列表。我必须 mergeAll 我的列表才能工作,但后来意识到无限的 3 和 5 倍数正是它们应该是的。所以,我添加了 2s 和 mergeAlld 一切,它们就出来了。之前,我愚蠢地认为mergeAll 不会处理无限列表,但它确实做到了。

    当 Haskell 中的列表是无限的时,Haskell 只计算所需的内容,即惰性列表。附加条件是它确实从一开始就计算。

    现在,由于 Haskell 倍数直到想要的极限,函数中不需要限制,即不再需要 takeWhile。速度提升令人难以置信,内存也降低了,

    以下是在我的 3GB RAM 的慢速家用 PC 上。

    tia = mergeAll.map (iterate (*2)) $
          mergeAll.map (iterate (*3)) $ iterate (*5) 1
    

    最后$ 10000 tia

    288325195312500000

    (0.02 秒,5,861,656 字节)

    2019 年 6 月 5 日 我学会了如何ghc -02 所以以下是 50000 Hammings 到 2.38E+30。这进一步证明了我的代码是垃圾。

    INIT    time    0.000s  (  0.000s elapsed)
    MUT     time    0.000s  (  0.916s elapsed)
    GC      time    0.047s  (  0.041s elapsed)
    EXIT    time    0.000s  (  0.005s elapsed)
    Total   time    0.047s  (  0.962s elapsed)
    
    Alloc rate    0 bytes per MUT second
    Productivity   0.0% of total user, 95.8% of total elapsed
    

    6.13.2019

    @Will Ness rawks。他对上面的tia 进行了简洁而优雅的修订,事实证明它的速度是GHCi 的五倍。当我ghc -O2 +RTS -s他对抗我的时候,我的速度是我的几倍。必须妥协。

    所以,我开始阅读我在 R. Bird 的 Thinking Functional with Haskell 中遇到的关于融合的内容,并且几乎立即尝试了这个。

    mai n = mergeAll.map (iterate (*n))
    mai 2 $ mai 3 $ iterate (*5) 1
    

    它与 Will 在 GHCi 中的 100K Hammings 匹配为 0.08,但真正让我感到惊讶的是(也适用于 100K Hammings。)这一点,尤其是经过的时间。 100K最高可达2.9e+38。

     TASKS: 3 (1 bound, 2 peak workers (2 total), using -N1)
    
      SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
    
      INIT    time    0.000s  (  0.000s elapsed)
      MUT     time    0.000s  (  0.002s elapsed)
      GC      time    0.000s  (  0.000s elapsed)
      EXIT    time    0.000s  (  0.000s elapsed)
      Total   time    0.000s  (  0.002s elapsed)
    
      Alloc rate    0 bytes per MUT second
    
      Productivity 100.0% of total user, 90.2% of total elapsed
    

    【讨论】:

    • GoalKicker.com “专业人士的 XXX”书籍是曾经是 SO 一部分的“文档”的副本(它自己这么说)。但是感谢您的链接,很高兴将所有内容集中在一个地方,格式精美,并且可以再次访问。在此之前根本不知道。
    • comp = sort $ [e | d &lt;- threes 26, e &lt;- list d] = sort $ [e | d &lt;- scanl (\a b -&gt; a*b) 1 $ replicate 26 3, e &lt;- scanl (\a b -&gt; a*b) d $ replicate 16 5] = sort $ concat [scanl (\a b -&gt; a*b) d $ replicate 16 5 | d &lt;- scanl (\a b -&gt; a*b) 1 $ replicate 26 3] = sort $ join [scanl (\a b -&gt; a*b) d $ replicate 16 5 | d &lt;- scanl (\a b -&gt; a*b) 1 $ replicate 26 3] = sort . join $ [scanl (*) d $ replicate 16 5 | d &lt;- scanl (*) 1 $ replicate 26 3] = sorted_join $ [take 17 $ iterate (*5) d | d &lt;- take 27 $ iterate (*3) 1] = sorted_join [ iterate (*5) d | d &lt;- iterate (*3) 1] =
    • = sorted_join ( map (iterate (*5)) (iterate (*3) 1) ) = sorted_join . map (iterate (*5)) . iterate (*3) $ 1
    • 一切都很好,令人印象深刻,非常感谢。我的启发是newcomb 中反映的变化。对角化 'newcomb 生成 all 唯一的 3 和 5。函数 ham2 现在提供 2 的 & 合并。现在,我可以像生成 3s 和 5s 一样生成 2s。我认为他们是棘手的。我想在继续其他逻辑之前尝试一下。我现在对函数进行对角化。这很迷人。 newcomb 值成为 2s 列表的初始值。 3s 是 3s & 5s 列表的初始值。它们可以连接和排序。
    • 请注意mergeAll sorted_join。 :) 另外,tia 可以更一般地写作foldr (\p -&gt; mergeAll . map (iterate (*p))) [1] [2,3,5]。 (你的意思是ghc -O2+RTS -s 当然不是-02)。
    猜你喜欢
    • 1970-01-01
    • 2017-04-10
    • 2011-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多