我们可以通过合并汉明数序列的适当倍数来有效地按顺序生成序列,这是经典算法。
如果n > 1 是可被p 整除的汉明数,则n/p 也是汉明数,如果m 是汉明数且p 是2、3 或5 之一,则m*p 也是一个汉明数。
所以我们可以将汉明数序列描述为
H = 1 : (2*H ∪ 3*H ∪ 5*H)
其中p*H 是所有汉明数与p 相乘得到的排序序列,∪ 表示排序后的并集(H = 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ... 也是如此,例如2*H = 2, 4, 6, 8, 10, 12, 16, 18, 20, 24, ... 和2*H ∪ 3*H = (2, 4, 6, 8, 10, 12, 16, ...) ∪ (3, 6, 9, 12, 15, ...) = (2, 3, 4, 6, 8, 9, 10, 12, 15, 16, ...))。
不过,这种算法有两个缺点。首先,它会产生必须在合并 (∪) 步骤中消除的重复项。其次,要生成N附近的汉明数,需要知道N/5、N/3和N/2附近的汉明数,最简单的方法是保持@987654340之间的序列部分@ 和 N 在内存中,对于大的 N 需要相当多的内存。
解决这两个问题的变体从 5 的幂序列开始,
P = 1, 5, 25, 125, 625, 3125, ...
并且在第一步中产生除了 3 或 5 之外没有素因数的数字,
T = P ∪ 3*T (= 1 : (5*P ∪ 3*T))
(一个数 n 除了 3 和 5 之外没有任何质因数,它要么是 5 的幂(n ∈ P),要么可以被 3 整除,n/3 除了 3 和 5 之外也没有质因数(@ 987654348@))。显然P和3*T这两个序列是不相交的,所以这里不会产生重复。
那么,我们最终得到汉明数序列
H = T ∪ 2*H
再次,很明显没有重复产生,并且要生成N附近的汉明数,我们需要知道N附近的序列T,这需要知道P附近的N和T 靠近 N/3,序列 H 靠近 N/2。在内存中只保留H 的部分在N/2 和N 之间,以及T 的部分在N/3 和N 之间需要的空间比保持H 的部分在N/5 之间要少得多和N 在内存中。
将my Haskell code 粗略翻译成 C++(毫无疑问是单调的,但我几乎没有写过 C++,而且我学到的 C++ 很古老)产生了
#include <iostream>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <gmpxx.h>
class Node {
public:
Node(mpz_class n) : val(n) { next = 0; };
mpz_class val;
Node *next;
};
class ListGenerator {
public:
virtual mpz_class getNext() = 0;
virtual ~ListGenerator() {};
};
class PurePowers : public ListGenerator {
mpz_class multiplier, value;
public:
PurePowers(mpz_class p) : multiplier(p), value(p) {};
mpz_class getNext() {
mpz_class temp = value;
value *= multiplier;
return temp;
}
// default destructor is fine here
// ~PurePowers() {}
};
class Merger : public ListGenerator {
mpz_class multiplier, thunk_value, self_value;
// generator of input sequence
// to be merged with our own output
ListGenerator *thunk;
// list of our output we need to remember
// to generate the next numbers
// Invariant: list is never empty, and sorted
Node *head, *tail;
public:
Merger(mpz_class p, ListGenerator *gen) : multiplier(p) {
thunk = gen;
// first output would be 1 (skipped here, though)
head = new Node(1);
tail = head;
thunk_value = thunk->getNext();
self_value = multiplier;
}
mpz_class getNext() {
if (thunk_value < self_value) {
// next value from the input sequence is
// smaller than the next value obtained
// by multiplying our output with the multiplier
mpz_class num = thunk_value;
// get next value of input sequence
thunk_value = thunk->getNext();
// and append our next output to the bookkeeping list
tail->next = new Node(num);
tail = tail->next;
return num;
} else {
// multiplier * head->val is smaller than next input
mpz_class num = self_value;
// append our next output to the list
tail->next = new Node(num);
tail = tail->next;
// and delete old head, which is no longer needed
Node *temp = head->next;
delete head;
head = temp;
// remember next value obtained from multiplying our own output
self_value = head->val * multiplier;
return num;
}
}
~Merger() {
// delete wrapped thunk
delete thunk;
// and list of our output
while (head != tail) {
Node *temp = head->next;
delete head;
head = temp;
}
delete tail;
}
};
// wrap list generator to include 1 in the output
class Hamming : public ListGenerator {
mpz_class value;
ListGenerator *thunk;
public:
Hamming(ListGenerator *gen) : value(1) {
thunk = gen;
}
// construct a Hamming number generator from a list of primes
// If the vector is empty or contains anything but primes,
// horrible things may happen, I don't care
Hamming(std::vector<unsigned long> primes) : value(1) {
std::sort(primes.begin(), primes.end());
ListGenerator *gn = new PurePowers(primes.back());
primes.pop_back();
while(primes.size() > 0) {
gn = new Merger(primes.back(), gn);
primes.pop_back();
}
thunk = gn;
}
mpz_class getNext() {
mpz_class num = value;
value = thunk->getNext();
return num;
}
~Hamming() { delete thunk; }
};
int main(int argc, char *argv[]) {
if (argc < 3) {
std::cout << "Not enough arguments provided.\n";
std::cout << "Usage: ./hamming start_index count [Primes]" << std::endl;
return 0;
}
unsigned long start, count, n;
std::vector<unsigned long> v;
start = strtoul(argv[1],NULL,0);
count = strtoul(argv[2],NULL,0);
if (argc == 3) {
v.push_back(2);
v.push_back(3);
v.push_back(5);
} else {
for(int i = 3; i < argc; ++i) {
v.push_back(strtoul(argv[i],NULL,0));
}
}
Hamming *ham = new Hamming(v);
mpz_class h;
for(n = 0; n < start; ++n) {
h = ham->getNext();
}
for(n = 0; n < count; ++n) {
h = ham->getNext();
std::cout << h << std::endl;
}
delete ham;
return 0;
}
在没有太低效的情况下完成这项工作:
$ ./hamming 0 20
1
2
3
4
5
6
8
9
10
12
15
16
18
20
24
25
27
30
32
36
$ time ./hamming 1000000 2
519381797917090766274082018159448243742493816603938969600000000000000000000000000000
519386406319142860380252256170487374054333610204770704575899579187200000000000000000
real 0m0.310s
user 0m0.307s
sys 0m0.003s
$ time ./hamming 100000000 1
181401839647817990674757344419030541037525904195621195857845491990723972119434480014547
971472123342746229857874163510572099698677464132177627571993937027608855262121141058201
642782634676692520729286408851801352254407007080772018525749444961547851562500000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000
real 0m52.138s
user 0m52.111s
sys 0m0.050s
(Haskell版本更快,GHC优化惯用Haskell比我优化unidiomatic C++更好)