#include <vector>
#include <chrono>
#include <cassert>
#include <map>
#include <random>
#include <cmath>
#include <cstdio>
#include <iostream>
#include <gmp.h>
#include <gmpxx.h>
std::minstd_rand rng;
gmp_randstate_t gmp_rng;
double prob[1011];
const int maxV = 510510;
#define Vfactors {2, 3, 5, 7, 11, 13, 17}
const int sqrtV = 715;
int sieve[maxV+1];
int primeps[maxV];
int primecnt = 0;
double logpq_sum[maxV];
int coprimes[92160];
std::uniform_int_distribution<> coprime_dist(0, 92159);
int coprimecnt = 0;
const double RAT = 510510. / 92160;
const double LOGV = 18.96157969569485;
void gen_factor(mpz_t N, int Nexp, double Nbase, mpz_t q, long int* qexp, double* qbase, mpz_t p, int* a, mpz_t Nq) {
const double invN = 1 / Nbase;
const double Nlog2 = Nexp + log2(Nbase);
int I;
for (I = 0; ; I++) {
int i = I;
double div = ldexp(invN*maxV, i-Nexp);
if (div >= 1) break;
prob[I] = (LOGV + i)*(1 + 1/(ldexp(maxV, i)-1)) + div*Nlog2;
}
int directI = I;
const double finvN = ldexp(invN, -Nexp);
prob[I++] = RAT * (logpq_sum[primecnt-1] + finvN * Nlog2 * (primecnt-1)); // small prime powers
std::discrete_distribution<> distribution(std::begin(prob), std::begin(prob)+I);
while (1) {
int J = distribution(rng);
if (J < directI) {
const int i = J;
mpz_urandomb(p, gmp_rng, i);
mpz_setbit(p, i);
mpz_mul_ui(p, p, maxV);
mpz_add_ui(p, p, coprimes[coprime_dist(rng)]);
if (mpz_cmp(p, N) > 0) continue;
long pexp;
double pbase = mpz_get_d_2exp(&pexp, p);
double p_d = mpz_get_d(p);
*a = 0;
double logp = pexp + log2(pbase);
double Nlogp = Nlog2 / logp;
double choice_probability = (LOGV + i) / logp + Nlogp * finvN * (ldexp(maxV, i) - 1);
#ifdef DEBUG
std::cout << choice_probability << '\n';
#endif
double f = std::uniform_real_distribution<>(0, choice_probability)(rng);
while (*a <= Nlogp && f >= 0) {
++*a;
f -= (ldexp(maxV, i) - 1) * pow(p_d, -*a) + (ldexp(maxV, i) - 1)*finvN;
}
if (*a > Nlogp) continue;
if (!mpz_probab_prime_p(p, 15)) continue;
mpz_pow_ui(q, p, *a);
} else { // small prime
double f = std::uniform_real_distribution<>(0, logpq_sum[primecnt-1] + finvN * Nlog2 * (primecnt-1) )(rng);
int l = 0;
int r = primecnt-2;
int ans = primecnt-1;
while (l <= r) {
int m = (l + r) / 2;
double v = logpq_sum[m] + finvN * Nlog2 * m;
if (v >= f) {
ans = m;
r = m-1;
} else {
l = m+1;
}
}
int pv = primeps[ans];
mpz_set_ui(p, pv);
double Nlogq = Nlog2 / log2(pv);
f = std::uniform_real_distribution<>(0, 1 + Nlogq * (pv-1) * finvN )(rng);
*a = 0;
while (*a <= Nlogq && f >= 0) {
++*a;
f -= (pv-1) * (pow(pv, -*a) + finvN);
}
if (*a > Nlogq) continue;
mpz_ui_pow_ui(q, pv, *a);
}
*qbase = mpz_get_d_2exp(qexp, q);
mpz_fdiv_q(Nq, N, q);
int is_odd = mpz_odd_p(Nq);
if (is_odd) mpz_add_ui(Nq, Nq, 1);
double Nqd = mpz_get_d(Nq);
if (!std::bernoulli_distribution(Nqd / (ldexp(Nbase / *qbase, Nexp - *qexp) + 1))(rng)) continue;
if (is_odd) mpz_sub_ui(Nq, Nq, 1);
return;
}
}
void bach(mpz_t v, mpz_t target, std::vector<mpz_class>& factors) {
if (mpz_cmp_ui(v, 1e7) < 0) {
unsigned int vI = mpz_get_ui(v);
int x = std::uniform_int_distribution<>(vI/2+1, vI)(rng);
mpz_set_ui(target, x);
factors.clear();
for (int d = 2; d*d <= x; d++) {
while (x % d == 0) {
factors.emplace_back(d);
x /= d;
}
}
if (x != 1) {
factors.emplace_back(x);
}
return;
}
mpz_t q, p, Nt;
mpz_inits(q, p, Nt, 0);
long int Nexp;
double Nbase = mpz_get_d_2exp(&Nexp, v);
while (1) {
int a;
long int qexp;
double qbase;
gen_factor(v, Nexp, Nbase, q, &qexp, &qbase, p, &a, Nt);
bach(Nt, target, factors);
long int targetexp;
double targetbase = mpz_get_d_2exp(&targetexp, target);
double logv = (log2(Nbase) + Nexp - 1)/(log2(qbase) + qexp + log2(targetbase) + targetexp);
if (std::bernoulli_distribution(logv)(rng)) {
for (int i = 0; i < a; i++) factors.emplace_back(p);
mpz_mul(target, target, q);
mpz_clears(q, p, Nt, 0);
return;
}
}
}
void gen(int n, mpz_t target, std::vector<mpz_class>& factors) {
mpz_t v;
mpz_init2(v, n);
mpz_setbit(v, n);
mpz_sub_ui(v, v, 1);
bach(v, target, factors);
}
int main() {
rng.seed(time(0));
gmp_randinit_lc_2exp_size(gmp_rng, 64);
time_t end = time(0) + 10;
sieve[0] = sieve[1] = 1;
for (int v : Vfactors) {
for (int i = v; i <= maxV; i += v) sieve[i] = 1;
}
for (int i = 1; i < maxV; i++) if (!sieve[i]) coprimes[coprimecnt++] = i;
for (int v : Vfactors) sieve[v] = 0;
double logpq_sum_v = 0;
for (int v = 2; v <= maxV; v++) {
if (!sieve[v]) {
if (v <= sqrtV)
for (int i = v*v; i <= maxV; i += v) sieve[i] = 1;
logpq_sum[primecnt] = logpq_sum_v += log2(v) / (v-1);
primeps[primecnt++] = v;
}
}
int i;
int tsz = 0;
int primecnt = 0;
for (i = 0; time(0) <= end; i++) {
mpz_t target;
mpz_init(target);
std::vector<mpz_class> factors;
gen(500, target, factors);
tsz += factors.size();
if (factors.size() == 1) primecnt++;
#ifdef OUTPUT
mpz_out_str(stdout, 10, target);
printf(" = ");
for (int j = 0; j < factors.size(); j++) {
if (j) printf(" * ");
std::cout << factors[j];
}
printf("\n");
#endif
mpz_clear(target);
}
std::cout << "generated " << i << " numbers\n";
std::cout << "prime probability " << primecnt/double(i) << ", expected apprx. " << 0.0028879 /* (Li(2^500) - Li(2^499)) / 2^499 */ << '\n';
std::cout << "average number of prime divisors " << tsz/double(i) << " expected apprx. " << log(500 * M_LN2) + 1.0345 /* B_2, see Wikipedia */ << '\n';
}
Try it online!
Can output around 15,000 numbers on my computer. Compile with -lgmp -lgmpxx -O3
. Please make sure there aren't other programs active while running this, because I've noticed that on my computer it changed the number by almost 1,000, even with just Firefox.
Explanation
This is an implemention of Bach's algorithm, although with a lot of optimizations.
The main function is gen_factor
, which given \$n\$ returns \$q = p^\alpha\$ with probability \$\log(p) \#(\frac{n}{2q}, \frac nq] = \log(p) \lfloor\frac{\frac{N}{q} + 1}2\rfloor\$, where \$\#(a, b]\$ is the number of integers in that range.
We approximate that the floor isn't important, and then use rejection sampling to get back the appropriate distribution.
After multiplying by (the constant) \$\frac2N\$, we get \$\log(p)(\frac1q+\frac1N)\$. Now we group the terms by \$p\$, and for a given \$p\$ we have its probability being $$\log(p) \sum_{\alpha=1}^{\lfloor \log_p(N) \rfloor} (\frac1{p^\alpha}+\frac1N) = \log(p) (\frac1{p-1} - \frac1{p^{\lfloor \log_p(N) \rfloor}} + \frac{\lfloor \log_p(N) \rfloor}N) \leq \frac{\log(p)}{p-1} + \frac{\log(N)}{N}$$
We split the range to \$[2, 510510)\$, and \$[510510 \cdot2^i, 510510 \cdot2^{i+1})\$ for all \$i\$. For the first range, we precalculate stuff to get numbers with the correct probability. For each of the \$[510510 \cdot2^i, 510510 \cdot2^{i+1})\$ ranges we notice that the probability is decreasing, so we can just take the probability for \$p = 510510\cdot 2^i\$ and use rejection sampling. To lower the amount of numbers we have to generate, we require the numbers to be coprime to \$510510 = 2\cdot3\cdot5\cdot7\cdot11\cdot13\cdot17\$, so we need to multiply the probability by \$\frac{510510}{\varphi(510510)}\$ for the other segment.