7

(this is exciting!) I know, the subject matter is well known. The state of the art (in Haskell as well as other languages) for efficient generation of unbounded increasing sequence of Hamming numbers, without duplicates and without omissions, has long been the following (AFAIK - and btw it is equivalent to the original Edsger Dijkstra's code too):

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

The question I'm asking is, can you find the way to make it more efficient in any significant measure? Is it still the state of the art or is it in fact possible to improve this to run twice faster and with better empirical orders of growth to boot?

If your answer is yes, please show the code and discuss its speed and empirical orders of growth in comparison to the above (it runs at about ~ n^1.05 .. n^1.10 for first few hundreds of thousands of numbers produced). Also, if it exists, can this efficient algorithm be extended to producing a sequence of smooth numbers with any given set of primes?

4

3 回答 3

11

如果一个常数因子(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/5必须h在内存中。在我的版本中,只有完整列表和之间的部分,h/2以及3-5列表和之间的部分需要在内存中。由于 3-5-list 更稀疏,并且 k-smooth 数字的密度降低,这两个列表部分需要的内存比完整列表的较大部分要少得多。hh/3h

两种算法产生第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)好吧,它看起来是恒定的,而且我认为这两种变体具有相同的计算复杂度,但我没有拿出纸笔来证明这一点,我也不打算这样做。

于 2012-09-18T17:56:09.540 回答
5

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

Daniel 在经典版本中正确识别了必须删除的重复生成的冗余。

截至 2012 年 8 月 26 日,原始发现 (AFAIK) 的功劳归于 Rosettacode.org 的贡献者 Ledrug。当然还有Daniel Fischer 的独立发现,这里(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 倍的加速。

于 2012-09-18T18:32:17.210 回答
0

好吧,这比我想象的要容易。这将在我家里的慢速 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

这意味着此代码的运行时增长的经验顺序高于二次(~n^2.13在 GHCi 提示下测量、解释)。

此外,该序列的三个悬垂的过度生产片段的大小各为~n^0.67ie ~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 乘以每个 s来生成,3s5s但是当2^0包括恒等式时,总的来说,它是 Hammings。

2019 年 3 月 25 日

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

我还发现mergeAllfromData.List.Orderedsortor sortand更快concat

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

然后,由于@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 日

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

Data.Universe.Helpers我开始使用@Daniel Wagner 和他的作品给我留下了深刻的印象和启发,+*++++后来又添加了我自己的无限列表。我必须按照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 字节)

6.5.2019 我学会了如何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

2019 年 6 月 13 日

@Will Ness rawks。他对上述内容进行了简洁而优雅的修改,tia事实证明它的速度是GHCi. 当我ghc -O2 +RTS -s反对我的时候,我的速度是我的几倍。必须达成妥协。

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

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

对于 100K Hammings,它与 Will's 匹配为 0.08,GHCi但真正让我感到惊讶的是(也适用于 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
于 2018-12-27T01:54:44.330 回答