Hamming Numbers

lua-users home
wiki

Difference (from prior major revision) (minor diff, author diff)

Changed: 20c20
although even shorter ones exist. The essence of the above solution is that Haskell evaluates lazily, so the infinite sequence hamming can be self-referential. That's possible to do in Lua, using a sort of promise. The code below implements just enough of a bignum implementation to be able to do the multiplications by 2, 3 and 5, and to print out the results. (The magic number 7305 results from the fact that Hamming number 7305 is 2^52; actually, a slightly larger limit could have been chosen. In practice, the bignum interface only adds about 33% to the cost of the computation so it could have been written as a pure bignum program; the interesting thing is that all of the code to implement Hamming is completely oblivious to the actual implementation of the numbers, provided that the implementation supports <, ==, and multiply-by-a-small-integer.
although even shorter ones exist. The essence of the above solution is that Haskell evaluates lazily, so the infinite sequence hamming can be self-referential. That's possible to do in Lua, using a sort of promise. The code below implements just enough of a bignum implementation to be able to do the multiplications by 2, 3 and 5, and to print out the results. (Hamming number 7305 is 2^52, so you'll need Bignum support if you want to compute more Hamming numbers than that. In practice, the bignum interface only adds about 33% to the cost of the computation; the interesting thing is that all of the code to implement Hamming is completely oblivious to the actual implementation of the numbers, provided that the implementation supports <, ==, and multiply-by-a-small-integer.

Changed: 28,47c28,47
50000 2.71 3472 54.2 69.4 2379126835648701693226200858624
100000 5.80 5048 58.0 50.5 290142196707511001929482240000000000000
150000 9.05 6060 60.3 40.4 136551478823710021067381144334863695872000000
200000 12.41 7176 62.0 35.9 4479571262811807241115438439905203543080960000000
250000 15.71 8092 62.8 32.4 29168801439715713801701411811093381120000000000000000
300000 19.09 8856 63.6 29.5 62864273786544799804687500000000000000000000000000000000
350000 22.38 9608 63.9 27.5 60133943158606419031489628585123616917982648729600000000000
400000 26.01 13276 65.0 33.2 30774090693237851027531250000000000000000000000000000000000000
450000 29.66 14356 65.9 31.9 9544028161703913537712243143807801346335324481500000000000000000
500000 33.49 14952 67.0 29.9 1962938367679548095642112423564462631020433036610484123229980468750
550000 37.25 15824 67.7 28.8 286188649932207038880389529600000000000000000000000000000000000000000
600000 41.06 16976 68.4 28.3 31118367413797724237140050340541118674764220486395061016082763671875000
650000 44.69 17620 68.8 27.1 2624748786803793305341077210881955201024000000000000000000000000000000000
700000 48.55 18508 69.4 26.4 177288702931066945536000000000000000000000000000000000000000000000000000000
750000 52.38 19696 69.8 26.3 9844116074857088479400896102109753641824661421606444941755765750304761446400
800000 56.15 20196 70.2 25.2 458936503790258814279745536000000000000000000000000000000000000000000000000000
850000 60.03 20620 70.6 24.3 18286806033409508387421738007105886936187744140625000000000000000000000000000000
900000 63.88 21772 71.0 24.2 632306706993546185913146473467350006103515625000000000000000000000000000000000000
950000 68.05 22756 71.6 24.0 19221158232427643481897048859403926759149694825267200000000000000000000000000000000
1000000 73.27 23084 73.3 23.1 519312780448388736089589843750000000000000000000000000000000000000000000000000000000
50000 1.99 3768 39.8 75.4 2379126835648701693226200858624
100000 4.41 5524 44.1 55.2 290142196707511001929482240000000000000
150000 6.87 6784 45.8 45.2 136551478823710021067381144334863695872000000
200000 9.42 7932 47.1 39.7 4479571262811807241115438439905203543080960000000
250000 11.99 8932 48.0 35.7 29168801439715713801701411811093381120000000000000000
300000 14.57 9944 48.6 33.1 62864273786544799804687500000000000000000000000000000000
350000 17.18 10768 49.1 30.8 60133943158606419031489628585123616917982648729600000000000
400000 20.06 14240 50.1 35.6 30774090693237851027531250000000000000000000000000000000000000
450000 23.10 16104 51.3 35.8 9544028161703913537712243143807801346335324481500000000000000000
500000 25.98 17392 52.0 34.8 1962938367679548095642112423564462631020433036610484123229980468750
550000 28.98 18724 52.7 34.0 286188649932207038880389529600000000000000000000000000000000000000000
600000 32.05 19256 53.4 32.1 31118367413797724237140050340541118674764220486395061016082763671875000
650000 35.27 20332 54.3 31.3 2624748786803793305341077210881955201024000000000000000000000000000000000
700000 38.13 21372 54.5 30.5 177288702931066945536000000000000000000000000000000000000000000000000000000
750000 41.34 21536 55.1 28.7 9844116074857088479400896102109753641824661421606444941755765750304761446400
800000 44.51 22280 55.6 27.9 458936503790258814279745536000000000000000000000000000000000000000000000000000
850000 47.59 23896 56.0 28.1 18286806033409508387421738007105886936187744140625000000000000000000000000000000
900000 50.64 23952 56.3 26.6 632306706993546185913146473467350006103515625000000000000000000000000000000000000
950000 54.02 26552 56.9 27.9 19221158232427643481897048859403926759149694825267200000000000000000000000000000000
1000000 57.36 26800 57.4 26.8 519312780448388736089589843750000000000000000000000000000000000000000000000000000000

Changed: 53,54c53,54
{{{
local function Seq(t, func)
{{{!Lua
do

Changed: 58,60c58,59
local rv = func()
self.tail = rv
setmetatable(self, nil)
local rv = self.gen(self)
self.tail, self.gen = rv, nil

Changed: 64c63,65
return setmetatable(t, meta)
function Seq(h, gen)
return setmetatable({head = h, gen = gen}, meta)
end

Changed: 67,69c68,70
local function SMap(seq, func)
return Seq({head = func(seq.head)},
function() return SMap(seq.tail, func) end)
function SMap(func, seq)
return Seq(func(seq.head),
function() return SMap(func, seq.tail) end)

Changed: 72c73
local function SMerge(seq1, seq2)
function SMerge(seq1, seq2)

Changed: 83c84
return Seq({head = head1}, step)
return Seq(head1, step)

Changed: 86,87c87,92
local function times(k)
return function(x) return x * k end
function Times(k)
if k then
return function(x) return x * k end
else
return function(x, y) return x * y end
end

Changed: 90c95,98
function Hamming(seq)
function Hamming()
local init = 1
if Bignum then init = Bignum(init) end
local seq = Seq(init)

Changed: 92,96c100,135
SMap(seq, times(2)), SMap(seq, times(3)), SMap(seq, times(5))
local function step()
return SMerge(seq2, SMerge(seq3, seq5))
end
return Seq(seq, function() return SMerge(seq2, SMerge(seq3, seq5)) end)
SMap(Times(2), seq), SMap(Times(3), seq), SMap(Times(5), seq)
function seq.gen() return SMerge(seq2, SMerge(seq3, seq5)) end
return seq
end

function SeqTail?(seq, k)
for i = 1, k do
seq = seq.tail
end
return seq
end

if arg then
local start, finish, inc = tonumber(arg[1]), tonumber(arg[2]), tonumber(arg[3])
if not start then start, finish, inc = 1, 20, 1 end
local h = SeqTail?(Hamming(), start-1)
print("hamming", start, h.head)
if finish then
while start + inc <= finish do
start = start + inc
h = SeqTail?(h, inc)
print("hamming", start, h.head)
end
end
end
}}}

Here are a few more interesting infinite sequence functions, including a Fibonacci generator (although I wouldn't recommend it in practice, it does run in constant space and more-or-less linear time.)

{{{!Lua
function SAlways(val)
return Seq(val, function(seq) return seq end)
end

function SDist(func, init, seq)
return Seq(init, function(self) return SDist(func, func(self.head, seq.head), seq.tail) end)

Changed: 99,104c138,140
function hamming(k)
local start = 1
if k > 7305 then start = Bignum(start) end
local h = Hamming({head = start})
for i = 2, k do h = h.tail end
return h.head
function SMap2(func, seq1, seq2)
return Seq(func(seq1.head, seq2.head),
function() return SMap2(func, seq1.tail, seq2.tail) end)

Added: 106a143,158
function Plus(k)
if k then
return function(x) return x + k end
else
return function(x, y) return x + y end
end
end

Iota = SDist(Plus(), 1, SAlways(1))

function Fib(i, j)
i, j = i or 1, j or 1
local seq = Seq(Bignum(i))
seq.tail = SDist(Plus(), Bignum(j), seq)
return seq
end

Changed: 110c162
{{{
{{{!Lua

Changed: 112,113c164,165
-- very limited bignum stuff; we only implement multiplication
-- (by less than the base) and comparison.
-- very limited bignum stuff; just enough for the examples here.
-- Please feel free to improve it.

Added: 158a211,249
-- Added so that Fib would work; other must be a Bignum
function meta:__add(other)
local result = {}
if self.n < other.n then self, other = other, self end
local n, m = self.n, other.n
local diff = n - m
local carry = 0
local result = {}
for i = m, 1, -1 do
local tmp = self[i + diff] + other[i] + carry
if tmp < base then
carry = 0
else
tmp = tmp - base
carry = 1
end
result[i + diff] = tmp
end
for i = diff, 1, -1 do
local tmp = self[i] + carry
if tmp < base then
carry = 0
else
tmp = tmp - base
carry = 1
end
result[i] = tmp
end
if carry > 0 then
n = n + 1
for i = n, 2, -1 do
result[i] = result[i - 1]
end
result[1] = carry
end
result.n = n
return setmetatable(result, meta)
end


Removed: 171d261


Changed: 175,186c265,272
{{{
for ((i=50000; $i<=1000000; i=$i+50000)); \
do /usr/bin/time -apl -o hamming.log ../src/lua \
-e "dofile 'hamming.lua'; print('result', $i, hamming($i))" >> hamming.log; \
done
egrep '(result|user|maximum)' hamming.log | \
lua -e 'local a = io.read"*a"; \
for n, res, time, size in \
string.gfind(a, "(%d+)%s+(%d+)%D+([%d.]+)%s+(%d+)") do \
print(string.format("%8i %8.2f %8i %6.1f %6.1f %s", \
n, time, size, (time*1e6)/n, (size*1e3)/n, res)) \
end' > hamming.res
{{{
for ((i = 50000; $i<=1000000; i = $i + 50000)) do /usr/bin/time -apl -o hamming.log ./hamming.lua $i >> hamming.log; done
egrep '(hamming|user|maximum)' hamming.log | \
lua -e 'local a = io.read"*a"; \
for n, res, time, size in string.gfind(a, "(%d+)%s+(%d+)%D+([%d.]+)%s+(%d+)") do \
print(string.format("%8i %8.2f %8i %6.1f %6.1f %s", \
n, time, size, (time*1e6)/n, (size*1e3)/n, res)) \
end' > hamming.res

Added: 187a274,279

--RiciLake

=== See Also ===

* HammingNumbersVariant

Hamming Numbers are numbers whose only prime factors are 2, 3 and 5. They are named after [Richard Hamming] but became famous (or notorious) after [Edsger Dijkstra] posed the question of how to efficiently enumerate them in numeric order.

The problem is frequently trotted out to explain why "x Programming" is better (particularly for the values of x: functional and logic). An O(n) solution (where n is the size of the sequence of numbers) is possible if you ignore the cost of manipulating large numbers. Buried deep in an interesting [thread] on [Lambda the Ultimate] is an analysis which indicates that the cost might be O(n^(4/3)).

The Haskell solution to this problem is delightfully brief:

-- Merges two infinite lists
merge :: (Ord a) => [a] -> [a] -> [a]
merge (x:xs)(y:ys)
  | x == y    = x : merge xs ys
  | x <  y    = x : merge xs (y:ys)
  | otherwise = y : merge (x:xs) ys

-- Lazily produce the hamming sequence
hamming :: [Integer]
hamming 
  = 1 : merge (map (2*) hamming) (merge (map (3*) hamming) (map (5*) hamming))

although even shorter ones exist. The essence of the above solution is that Haskell evaluates lazily, so the infinite sequence hamming can be self-referential. That's possible to do in Lua, using a sort of promise. The code below implements just enough of a bignum implementation to be able to do the multiplications by 2, 3 and 5, and to print out the results. (Hamming number 7305 is 2^52, so you'll need Bignum support if you want to compute more Hamming numbers than that. In practice, the bignum interface only adds about 33% to the cost of the computation; the interesting thing is that all of the code to implement Hamming is completely oblivious to the actual implementation of the numbers, provided that the implementation supports <, ==, and multiply-by-a-small-integer.

I checked the performance of the program by computing hamming(k) for values of k from 50,000 to 1,000,000; the performance does appear to be roughly O(n) in both space and time, although I made no attempt to optimise and the space consumption is somewhat porcine.

                             usec    RSS
       n     sec.       Mb     /n   kb/n   value
 -------     ----    -----   ----   ----   --------------------------------------------------------------------
   50000     1.99     3768   39.8   75.4   2379126835648701693226200858624
  100000     4.41     5524   44.1   55.2   290142196707511001929482240000000000000
  150000     6.87     6784   45.8   45.2   136551478823710021067381144334863695872000000
  200000     9.42     7932   47.1   39.7   4479571262811807241115438439905203543080960000000
  250000    11.99     8932   48.0   35.7   29168801439715713801701411811093381120000000000000000
  300000    14.57     9944   48.6   33.1   62864273786544799804687500000000000000000000000000000000
  350000    17.18    10768   49.1   30.8   60133943158606419031489628585123616917982648729600000000000
  400000    20.06    14240   50.1   35.6   30774090693237851027531250000000000000000000000000000000000000
  450000    23.10    16104   51.3   35.8   9544028161703913537712243143807801346335324481500000000000000000
  500000    25.98    17392   52.0   34.8   1962938367679548095642112423564462631020433036610484123229980468750
  550000    28.98    18724   52.7   34.0   286188649932207038880389529600000000000000000000000000000000000000000
  600000    32.05    19256   53.4   32.1   31118367413797724237140050340541118674764220486395061016082763671875000
  650000    35.27    20332   54.3   31.3   2624748786803793305341077210881955201024000000000000000000000000000000000
  700000    38.13    21372   54.5   30.5   177288702931066945536000000000000000000000000000000000000000000000000000000
  750000    41.34    21536   55.1   28.7   9844116074857088479400896102109753641824661421606444941755765750304761446400
  800000    44.51    22280   55.6   27.9   458936503790258814279745536000000000000000000000000000000000000000000000000000
  850000    47.59    23896   56.0   28.1   18286806033409508387421738007105886936187744140625000000000000000000000000000000
  900000    50.64    23952   56.3   26.6   632306706993546185913146473467350006103515625000000000000000000000000000000000000
  950000    54.02    26552   56.9   27.9   19221158232427643481897048859403926759149694825267200000000000000000000000000000000
 1000000    57.36    26800   57.4   26.8   519312780448388736089589843750000000000000000000000000000000000000000000000000000000

So here's the code:

do 
  local meta = {}
  function meta:__index(k)
    if k == "tail" then
      local rv = self.gen(self)
      self.tail, self.gen = rv, nil
      return rv
    end
  end
  function Seq(h, gen)
    return setmetatable({head = h, gen = gen}, meta)
  end
end

function SMap(func, seq)
  return Seq(func(seq.head),
             function() return SMap(func, seq.tail) end)
end

function SMerge(seq1, seq2)
  local head1, head2 = seq1.head, seq2.head
  local step
  if head1 < head2 then
    function step() return SMerge(seq1.tail, seq2) end
  elseif head2 < head1 then
    function step() return SMerge(seq1, seq2.tail) end
    head1 = head2
  else
    function step() return SMerge(seq1.tail, seq2.tail) end
  end
  return Seq(head1, step)
end

function Times(k)
  if k then
    return function(x) return x * k end
  else
    return function(x, y) return x * y end
  end
end

function Hamming()
  local init = 1
  if Bignum then init = Bignum(init) end
  local seq = Seq(init)
  local seq2, seq3, seq5 =
        SMap(Times(2), seq), SMap(Times(3), seq), SMap(Times(5), seq)  
  function seq.gen() return SMerge(seq2, SMerge(seq3, seq5)) end
  return seq
end

function SeqTail(seq, k)
  for i = 1, k do
    seq = seq.tail
  end
  return seq
end

if arg then
  local start, finish, inc = tonumber(arg[1]), tonumber(arg[2]), tonumber(arg[3])
  if not start then start, finish, inc = 1, 20, 1 end
  local h = SeqTail(Hamming(), start-1)
  print("hamming", start, h.head)
  if finish then
    while start + inc <= finish do
      start = start + inc
      h = SeqTail(h, inc)
      print("hamming", start, h.head)
    end
  end
end

Here are a few more interesting infinite sequence functions, including a Fibonacci generator (although I wouldn't recommend it in practice, it does run in constant space and more-or-less linear time.)

function SAlways(val)
  return Seq(val, function(seq) return seq end)
end

function SDist(func, init, seq)
  return Seq(init, function(self) return SDist(func, func(self.head, seq.head), seq.tail) end)
end

function SMap2(func, seq1, seq2)
  return Seq(func(seq1.head, seq2.head),
             function() return SMap2(func, seq1.tail, seq2.tail) end)
end

function Plus(k)
  if k then
    return function(x) return x + k end
  else
    return function(x, y) return x + y end
  end
end

Iota = SDist(Plus(), 1, SAlways(1))

function Fib(i, j)
  i, j = i or 1, j or 1
  local seq = Seq(Bignum(i))
  seq.tail = SDist(Plus(), Bignum(j), seq)
  return seq
end

and here's the simple Bignum implementation (define this first if you want to test the above code):

do
  -- very limited bignum stuff; just enough for the examples here.
  -- Please feel free to improve it.
  local base = 1e15
  local fmt = "%015.0f"
  local meta = {}
  function meta:__lt(other)
    if self.n ~= other.n then return self.n < other.n end
    for i = 1, self.n do
      if self[i] ~= other[i] then return self[i] < other[i] end
    end
  end
  function meta:__eq(other)
    if self.n == other.n then
      for i = 1, self.n do
        if self[i] ~= other[i] then return false end
      end
      return true
    end
  end
  function meta:__mul(k)
    -- If the base where a multiple of all possible multipliers, then
    -- we could figure out the length of the result directly from the
    -- first "digit". On the other hand, printing the numbers would be
    -- difficult. So we accept the occasional overflow.
    local offset = 0
    if self[1] * k >= base then offset = 1 end
    local carry = 0
    local result = {}
    local n = self.n
    for i = n, 1, -1 do
      local tmp = self[i] * k + carry
      local digit = tmp % base
      carry = (tmp - digit) / base
      result[offset + i] = digit
    end
    if carry ~= 0 then
      n = n + 1
      if offset == 0 then
        for i = n, 2, -1 do
          result[i] = result[i - 1]
        end
      end
      result[1] = carry
    end
    result.n = n
    return setmetatable(result, meta)
  end
  -- Added so that Fib would work; other must be a Bignum
  function meta:__add(other)
    local result = {}
    if self.n < other.n then self, other = other, self end
    local n, m = self.n, other.n
    local diff = n - m
    local carry = 0
    local result = {}
    for i = m, 1, -1 do
      local tmp = self[i + diff] + other[i] + carry
      if tmp < base then
        carry = 0
      else
        tmp = tmp - base
        carry = 1
      end
      result[i + diff] = tmp
    end
    for i = diff, 1, -1 do
      local tmp = self[i] + carry
      if tmp < base then
        carry = 0
      else
        tmp = tmp - base
        carry = 1
      end
      result[i] = tmp
    end
    if carry > 0 then
      n = n + 1
      for i = n, 2, -1 do
        result[i] = result[i - 1]
      end
      result[1] = carry
    end
    result.n = n
    return setmetatable(result, meta)
  end

  function meta:__tostring()
    local tmp = {}
    tmp[1] = ("%.0f"):format(self[1])
    for i = 2, self.n do
      tmp[i] = fmt:format(self[i])
    end
    return table.concat(tmp)
  end
  function Bignum(k)
    return setmetatable({k, n = 1}, meta)
  end
end

For reference, the time/space chart was created (except for the heading) with the following (which will probably only work on FreeBSD or similar):

for ((i = 50000; $i<=1000000; i = $i + 50000)) do /usr/bin/time -apl -o hamming.log ./hamming.lua $i >> hamming.log; done
egrep '(hamming|user|maximum)' hamming.log | \
 lua -e 'local a = io.read"*a"; \
         for n, res, time, size in string.gfind(a, "(%d+)%s+(%d+)%D+([%d.]+)%s+(%d+)") do \
           print(string.format("%8i %8.2f %8i %6.1f %6.1f   %s", \
                               n, time, size, (time*1e6)/n, (size*1e3)/n, res)) \
         end' > hamming.res

--RiciLake

See Also


RecentChanges · preferences
edit · history
Last edited May 28, 2007 8:42 pm GMT (diff)