7

(これはエキサイティングです!)私は知っています、主題はよく知られています。ハミング数の無制限の増加シーケンスを、重複や省略なしに効率的に生成するための最新技術(Haskellおよび他の言語)は、長い間次のとおりです(AFAIK-そしてところで、元のEdsgerDijkstraのコードと同等です)それも):

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

私が尋ねている質問は、重要な手段でそれをより効率的にする方法を見つけることができるかということです。それはまだ最先端の技術ですか、それとも実際にこれを改善して2倍速く実行し、より良い経験的な成長順序で起動することは可能ですか?

答えが「はい」の場合は、コードを表示し、上記と比較した場合の速度と経験的な成長順序について話し合ってください(~ n^1.05 .. n^1.10最初の数十万の数が生成されます)。また、存在する場合、この効率的なアルゴリズムを拡張して、任意の素数のセットで滑らかな数のシーケンスを生成できますか?

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の間のリストの一部がメモリ内にある必要があります。私のバージョンでは、完全なリストの間の部分と、3-5リストの間の部分だけがメモリに存在する必要があります。3-5-リストははるかにまばらであり、k-smooth数の密度が低下するため、これら2つのリスト部分は、完全なリストの大部分よりもはるかに少ないメモリを必要とします。h/5hh/2hh/3h

2つのアルゴリズムが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

つまり、基本的には、ダニエル・フィッシャーが答えたので、最近これに出くわしたと言えます。これは、ダイクストラ以来、古典的なコードが古くから知られていたため、エキサイティングな開発だと思います。

ダニエルは、古典的なバージョンでは、複製生成の冗長性を正しく識別しました。

最初の発見(AFAIK)のクレジットは、2012年8月26日の時点で、Rosettacode.orgの寄稿者であるLedrugに帰属します。そしてもちろん、ダニエル・フィッシャーによる独立した発見、ここ(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回のハミングが実行されます。今日の午後の仕事と600未満のより速いPC時間はゼロ秒として出てきました。

これはハミングスからハミングスを取ります。これは、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

これは、このコードの実行時の経験的な成長順序が2次式を上回っていることを意味します(~n^2.13GHCiプロンプトで測定、解釈)。

また、シーケンスの3つのぶら下がっている過剰生成されたセグメントのサイズはそれぞれ~n^0.67です~n^(2/3)

さらに、このコードは遅延しません。結果のシーケンスの最初の要素にアクセスできるのは、最後の要素が計算された後のみです。

問題の最先端のコードは線形であり、関心のあるポイントを超えて正確に0個の要素を過剰生成し、適切に怠惰です。つまり、すぐに数値の生成を開始します。

したがって、このポスターによる以前の回答よりも大幅に改善されていますが、上位2つの回答に表示されている改善は言うまでもなく、元の回答よりも大幅に悪化しています。

2018年12月31日

最高の人だけが教育します。@Will Nessは、GoalKicker.comの「Haskellfor 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])

リストは、すべてのsにそれぞれを掛けてtwos生成できますが、IDが含まれている場合は、合計でハミングになります。2^e3s5s2^0

2019年3月25日

さて、ついに。私はこれを少し前に知っていましたが、最後に過剰な値がないと実装できませんでした。問題は、デカルト積の結果である超過分を生成しない方法でした。Excelをよく使用しますが、デカルト積ワークシートから除外する値のパターンを確認できませんでした。じゃあ、エウレカ!関数は、各リードファクターのリストを生成します。各リストの値を制限する値は、最初のリストの終点です。これが行われると、すべてのハミングが過剰に生成されます。

ハミングの2つの関数。1つ目は、新しい3と5のリストで、2で倍数を作成するために使用されます。倍数はハミングです。

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また、暗黙のパラメーターで使用することを選択しました。

mergeAllまた、 fromの方がorと。Data.List.Orderedよりも高速であることがわかりました。sortsortconcat

また、サブリストが作成されるのも気に入っているので、データをはるかに簡単に分析できます。

次に、@ Will Nessが原因で、よりクリーンなコードを作成するiterate代わりに切り替えました。scanlまた、@ Will Nessのために、最後の2のリストの使用をやめ、すべての長さを決定する1つの値に切り替えました。

再帰的に定義されたリストの方が効率的だと思います。前の数値に係数を掛けたものです。

関数を2つに分割するだけでは違いがないため、3と5の倍数は次のようになります。

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

そして、それぞれ2に3と5の積を掛けたもの

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日

私は無限で再帰的なリストをとても好むので、これらを無限にすることに少し夢中になりました。

私は@DanielWagnerにとても感銘を受け、インスピレーションを得ました。彼Data.Universe.Helpersは使い始めましたが+*++++その後、自分の無限のリストを追加しました。私はmergeAll自分のリストを機能させる必要がありましたが、その後、無限の3と5の倍数がまさにそれらが本来あるべきものであることに気づきました。それで、2を追加してmergeAllすべてをdして、それらが出てきました。以前、私は愚かなmergeAllことに無限のリストを処理しないと思っていましたが、それは最も素晴らしいことです。

Haskellでリストが無限大の場合、Haskellは必要なものだけを計算します。つまり、怠惰です。補助は、それが最初から計算するということです。

さて、Haskellは必要なものの限界まで倍数になるので、関数に限界は必要ありません。つまり、それ以上は必要ありませんtakeWhile。スピードアップは信じられないほどで、メモリも低下しました、

以下は、3GBのRAMを搭載した私の遅い自宅のPCです。

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

最後の$は10000ティアを取る

288325195312500000

(0.02秒、5,861,656バイト)

6.5.2019 方法を学びましたghc -02ので、以下は50000ハミングから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

@WillNessrawks。彼は上記のクリーンでエレガントな改訂版を提供し、tiaそれはで5倍速いことが証明されました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

ウィルの0.08と100Kハミングで一致しましたGHCiが、私が本当に驚いたのは(100Kハミングでも)これと特に経過時間です。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 に答える