涵数

lua-users home
wiki

涵数是指仅包含 2、3 和 5 作为质因子的数字。它们以 [理查德·汉明] 的名字命名,但因 [埃兹格尔·迪克斯特拉] 提出的如何有效地按数字顺序枚举它们的问题而闻名(或臭名昭著)。

这个问题经常被用来解释为什么“x 编程”更好(特别是对于 x 的值:functionallogic)。如果你忽略操作大数字的成本,那么可以实现一个 O(n) 的解决方案(其中 n 是数字序列的大小)。在一个有趣的 [线程] 中,深入 [Lambda the Ultimate] 的分析表明,成本可能是 O(n^(4/3))

Haskell 对这个问题的解决方案非常简洁

-- 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))

尽管存在更短的解决方案。上述解决方案的本质是 Haskell 是惰性求值的,因此无限序列 hamming 可以是自引用的。这在 Lua 中也是可能的,使用一种类似于承诺的东西。下面的代码实现了足够的大数实现,以便能够进行 2、3 和 5 的乘法,并打印出结果。(涵数 7305 是 2^52,所以如果你想计算比这更多的涵数,你需要大数支持。实际上,大数接口只增加了大约 33% 的计算成本;有趣的是,所有实现涵数的代码都完全不知道数字的实际实现,只要实现支持 <、== 和乘以一个小整数即可。

我通过计算 k 从 50,000 到 1,000,000 的 hamming(k) 值来检查程序的性能;性能似乎在空间和时间上都大致为 O(n),尽管我没有尝试优化,并且空间消耗有点大。

                             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

所以这是代码

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

这里还有一些更有趣的无限序列函数,包括一个斐波那契生成器(虽然我不建议在实践中使用它,但它确实在常数空间和大致线性时间内运行)。

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

这里是一个简单的 Bignum 实现(如果你想测试上面的代码,请先定义它)

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

作为参考,时间/空间图表(除了标题)是用以下代码创建的(可能只在 FreeBSD 或类似系统上有效)

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

另请参阅


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2007 年 5 月 28 日下午 9:42 GMT (差异)