使用一组素数以升序生成整数

时间:2012-04-12 15:02:11

标签: algorithm primes hamming-numbers smooth-numbers

我有一组素数,我必须使用递增顺序的那些素因子生成整数。

例如,如果集合是 p = {2,5}那么我的整数应该是1,2,4,5,8,10,16,20,25 ......

有没有有效的算法来解决这个问题?

4 个答案:

答案 0 :(得分:11)

删除一个数字并将其所有倍数(通过集合中的素数)重新插入优先级队列错误(就问题而言) - 即它产生正确的序列,但效率低

它在两个方面效率低下 - 首先,过度生成序列;第二,每个PriorityQueue操作都会产生额外的成本(操作remove_topinsert通常都不是 O(1),当然不在任何基于列表或树的PriorityQueue实现中)。

高效的 O(n)算法在生成序列时将指针保留回序列本身,以便在 O(1)中查找并附加下一个数字。在伪代码中:

  return array h where
    h[0]=1; n=0; ps=[2,3,5, ... ]; // base primes
    is=[0 for each p in ps];       // indices back into h
    xs=[p for each p in ps]        // next multiples: xs[k]==ps[k]*h[is[k]]
    repeat:
      h[++n] := minimum xs
      for each (i,x,p) in (is,xs,ps):
        if( x==h[n] )
          { x := p*h[++i]; }       // advance the minimal multiple/pointer

对于每个最小倍数,它推进其指针,同时计算其下一个多值。这实际上有效地实现了PriorityQueue但具有重要的区别 - 它是之前的,而不是之后;除序列本身外,它不会创建任何额外的存储空间;并且它的大小是恒定的(只有 k 数字,对于 k 基本素数),而过去的优先级PriorityQueue的大小随着我们沿着序列的进展而增长(在这种情况下) Hamming序列,基于 3 素数的集合,作为 n 2/3 ,用于 n 的数字序列)。


经典Hamming sequence in Haskell基本上是相同的算法:

h = 1 : map (2*) h `union` map (3*) h `union` map (5*) h

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

我们可以使用foldi函数生成任意基本素数的smooth numbers(请参阅Wikipedia)以折叠树中的列表提高效率的方式,创建一个固定大小的比较树:

smooth base_primes = h   where       -- strictly increasing base_primes  NB!
    h = 1 : foldi g [] [map (p*) h | p <- base_primes]
    g (x:xs) ys = x : union xs ys

foldi f z []     = z
foldi f z (x:xs) = f x (foldi f z (pairs f xs))

pairs f (x:y:t)  = f x y : pairs f t
pairs f t        = t

也可以在 O(n 2/3 n 成员周围直接计算汉明序列的切片 >)时间,通过直接枚举三元组并通过对数评估它们的值logval(i,j,k) = i*log 2+j*log 3+k*log 5。此Ideone.com test entry 1.12 0.05 中计算1 billionth Hamming number(2016-08-18:由于使用{{1}而导致的主要加速在可能的情况下代替默认的Int,即使在32位上也是如此;由于@GordonBGood建议的调整,将带尺寸复杂度降低到O(n 1/3 ))。

我们在this answer中对此进行了详细讨论,我们也在其中找到了完整的归属地:

Integer

这也可以推广到 k 基本素数,可能在 O(n (k-1)/ k 时间内运行


请参阅this SO entry以了解重要的后续发展。另外,this answer很有意思。另一个related answer

答案 1 :(得分:8)

基本思想是1是集合的成员,对于集合 n 的每个成员,所以2 n 和5 n 是该集的成员。因此,首先输出1,然后将2和5推入优先级队列。然后,重复弹出优先级队列的前面项,如果它与前一个输出不同,则输出它,然后将2次和5次数推入优先级队列。

谷歌的“汉明号码”或“常规号码”或转到A003592了解详情。

-----稍后添加-----

我决定在午餐时间花几分钟编写一个程序来实现上述算法,使用Scheme编程语言。首先,here是使用配对堆算法的优先级队列的库实现:

(define pq-empty '())
(define pq-empty? null?)

(define (pq-first pq)
  (if (null? pq)
      (error 'pq-first "can't extract minimum from null queue")
      (car pq)))

(define (pq-merge lt? p1 p2)
  (cond ((null? p1) p2)
        ((null? p2) p1)
        ((lt? (car p2) (car p1))
          (cons (car p2) (cons p1 (cdr p2))))
        (else (cons (car p1) (cons p2 (cdr p1))))))

(define (pq-insert lt? x pq)
  (pq-merge lt? (list x) pq))

(define (pq-merge-pairs lt? ps)
  (cond ((null? ps) '())
        ((null? (cdr ps)) (car ps))
        (else (pq-merge lt? (pq-merge lt? (car ps) (cadr ps))
                            (pq-merge-pairs lt? (cddr ps))))))

(define (pq-rest lt? pq)
  (if (null? pq)
      (error 'pq-rest "can't delete minimum from null queue")
      (pq-merge-pairs lt? (cdr pq))))

现在为算法。函数f有两个参数,一组 ps 中的数字列表,以及从输出头输出的项目的数量 n 。算法略有改变;通过按1初始化优先级队列,然后开始提取步骤。变量 p 是先前的输出值(最初为0), pq 是优先级队列, xs 是输出列表,它是在相反的顺序。这是代码:

(define (f ps n)
  (let loop ((n n) (p 0) (pq (pq-insert < 1 pq-empty)) (xs (list)))
    (cond ((zero? n) (reverse xs))
          ((= (pq-first pq) p) (loop n p (pq-rest < pq) xs))
          (else (loop (- n 1) (pq-first pq) (update < pq ps)
                      (cons (pq-first pq) xs))))))

对于不熟悉Scheme的人,loop是一个本地定义的函数,递归调用,cond是if-else链的头部;在这种情况下,有三个cond子句,每个子句都带有谓词和结果,随后对谓词为真的第一个子句进行求值。谓词(zero? n)终止递归并以正确的顺序返回输出列表。谓词(= (pq-first pq) p)表示先前已输出优先级队列的当前头部,因此通过在第一个项目之后重复使用优先级队列的其余部分来跳过它。最后,else谓词(始终为true)标识要输出的新数字,因此它递减计数器,将优先级队列的当前头保存为新的先前值,更新优先级队列以添加当前数字的新子节点,并将优先级队列的当前头插入累加输出。

由于更新优先级队列以添加当前号码的新子节点并非易事,因此该操作将被提取到单独的函数中:

(define (update lt? pq ps)
  (let loop ((ps ps) (pq pq))
    (if (null? ps) (pq-rest lt? pq)
      (loop (cdr ps) (pq-insert lt? (* (pq-first pq) (car ps)) pq)))))

该函数循环遍历ps集的元素,依次将每个元素插入优先级队列;当if列表用尽时,ps返回更新的优先级队列,减去旧的头部。递归步骤使用ps剥离cdr列表的头部,并将优先级队列头部和ps列表头部的乘积插入优先级队列。

以下是该算法的两个示例:

> (f '(2 5) 20)
(1 2 4 5 8 10 16 20 25 32 40 50 64 80 100 125 128 160 200 250)
> (f '(2 3 5) 20)
(1 2 3 4 5 6 8 9 10 12 15 16 18 20 24 25 27 30 32 36)

您可以在http://ideone.com/sA1nn运行该程序。

答案 2 :(得分:3)

这个二维探索算法并不精确,但适用于前25个整数,然后混合625和512.

Powers of 2 and 5

n = 0
exp_before_5 = 2
while true
  i = 0
  do
    output 2^(n-exp_before_5*i) * 5^Max(0, n-exp_before_5*(i+1))
    i <- i + 1
  loop while n-exp_before_5*(i+1) >= 0
  n <- n + 1
end while

答案 3 :(得分:3)

根据user448810的回答,这是一个使用STL中的堆和向量的解决方案。
现在,堆通常输出最大值,因此我们将数字的负数存储为变通方法(自a>b <==> -a<-b起)。

#include <vector>
#include <iostream>
#include <algorithm>

int main()
{
    std::vector<int> primes;
    primes.push_back(2);
    primes.push_back(5);//Our prime numbers that we get to use

    std::vector<int> heap;//the heap that is going to store our possible values
    heap.push_back(-1);
    std::vector<int> outputs;
    outputs.push_back(1);
    while(outputs.size() < 10)
    {
        std::pop_heap(heap.begin(), heap.end());
        int nValue = -*heap.rbegin();//Get current smallest number
        heap.pop_back();
        if(nValue != *outputs.rbegin())//Is it a repeat?
        {
            outputs.push_back(nValue);
        }
        for(unsigned int i = 0; i < primes.size(); i++)
        {
            heap.push_back(-nValue * primes[i]);//add new values
            std::push_heap(heap.begin(), heap.end());
        }
    }
    //output our answer
    for(unsigned int i = 0; i < outputs.size(); i++)
    {
        std::cout << outputs[i] << " ";
    }
    std::cout << std::endl;
}

输出:

1 2 4 5 8 10 16 20 25 32