C++ Find all bases such that P in those bases ends with the decimal representation of Q
how do I know where (after what base) to stop checking
Eventually, the base will become great enough that P will be represented with less digits than the number of decimal digits required to represent Q.
A more strict limit can be found considering the first base which produces a representation of P which is less than the one consisting of the decimal digits of Q. E.g. (71)10 = (12)69.
The following code shows a possible implementation.
#include <algorithm>
#include <cassert>
#include <iterator>
#include <vector>
auto digits_from( size_t n, size_t base )
{
std::vector<size_t> digits;
while (n != 0) {
digits.push_back(n % base);
n /= base;
}
if (digits.empty())
digits.push_back(0);
return digits;
}
auto find_bases(size_t P, size_t Q)
{
std::vector<size_t> bases;
auto Qs = digits_from(Q, 10);
// I'm using the digit with the max value to determine the starting base
auto it_max = std::max_element(Qs.cbegin(), Qs.cend());
assert(it_max != Qs.cend());
for (size_t base = *it_max + 1; ; ++base)
{
auto Ps = digits_from(P, base);
// We can stop when the base is too big
if (Ps.size() < Qs.size() ) {
break;
}
// Compare the first digits of P in this base with the ones of P
auto p_rbegin = std::reverse_iterator<std::vector<size_t>::const_iterator>(
Ps.cbegin() + Qs.size()
);
auto m = std::mismatch(Qs.crbegin(), Qs.crend(), p_rbegin, Ps.crend());
// All the digits match
if ( m.first == Qs.crend() ) {
bases.push_back(base);
}
// The digits form a number which is less than the one formed by Q
else if ( Ps.size() == Qs.size() && *m.first > *m.second ) {
break;
}
}
return bases;
}
int main()
{
auto bases = find_bases(71, 13);
assert(bases[0] == 4 && bases[1] == 68);
}
Edit
As noted by One Lyner, the previous brute force algorithm misses some corner cases and it's impractical for larger values of Q. In the following I'll address some of the possible optimizations.
Let's call m the number of decimal digit of Q, we want
(P)b = ... + qnbn + qn-1bn-1 + ... + q1b1 + q0 where m = n + 1
Different approaches can be explored, based on the number of digits of Q
Q has only one digit (so m = 1)
The previous equation reduces to
(P)b = q0
- When P < q0 there are no solutions.
- If P == q0 all the values greater than min(q0, 2) are valid solutions.
- When P > q0 we have to check all (not really all, see the next item) the bases in [2, P - q0].
Q has only two digits (so m = 2)
Instead of checking all the possible candidates, as noted in One Lyner's answer, we can note that as we are searching the divisors of p = P - q0, we only need to test the values up to
bsqrt = sqrt(p) = sqrt(P - q0)
Because
if p % b == 0 than p / b is another divisor of p
The number of candidates can be ulteriorly limited using more sophisticated algorithms involving primes detection, as showed in One Lyner's answer. This will greatly reduce the running time of the search for the bigger values of P.
In the test program that follows I'll only limit the number of sample bases to bsqrt, when m <= 2.
The number of decimal digits of Q is greater than 2 (so m > 2)
We can introduce two more limit values
blim = mth root of P
It's the last radix producing a representation of P with more digits than Q. After that, there is only one radix such that
(P)b == qnbn + qn-1bn-1 + ... + q1b1 + q0
As P (and m) increases, blim becomes more and more smaller than bsqrt.
We can limit the search of the divisors up to blim and then find the last solution (if exists) in a few steps applying a root finding algorithm such as the Newton's method or a simple bisection one.
If big values are involved and fixed-sized numeric types are used, overflow is a concrete risk.
In the following program (admittedly quite convoluted), I tried to avoid it checking the calculations which produce the various roots and using a simple beisection method for the final step which doesn't evaluate the polynomial (like a Newton step would require), but just compares the digits.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <climits>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <type_traits>
#include <vector>
namespace num {
template< class T
, typename std::enable_if_t<std::is_integral_v<T>, int> = 0 >
auto abs(T value)
{
if constexpr ( std::is_unsigned_v<T> ) {
return value;
}
using U = std::make_unsigned_t<T>;
// See e.g. https://stackoverflow.com/a/48612366/4944425
return U{ value < 0 ? (U{} - value) : (U{} + value) };
}
template <class T>
constexpr inline T sqrt_max {
std::numeric_limits<T>::max() >> (sizeof(T) * CHAR_BIT >> 1)
};
constexpr bool safe_sum(std::uintmax_t& a, std::uintmax_t b)
{
std::uintmax_t tmp = a + b;
if ( tmp <= a )
return false;
a = tmp;
return true;
}
constexpr bool safe_multiply(std::uintmax_t& a, std::uintmax_t b)
{
std::uintmax_t tmp = a * b;
if ( tmp / a != b )
return false;
a = tmp;
return true;
}
constexpr bool safe_square(std::uintmax_t& a)
{
if ( sqrt_max<std::uintmax_t> < a )
return false;
a *= a;
return true;
}
template <class Ub, class Ue>
auto safe_pow(Ub base, Ue exponent)
-> std::enable_if_t< std::is_unsigned_v<Ub> && std::is_unsigned_v<Ue>
, std::optional<Ub> >
{
Ub power{ 1 };
for (;;) {
if ( exponent & 1 ) {
if ( !safe_multiply(power, base) )
return std::nullopt;
}
exponent >>= 1;
if ( !exponent )
break;
if ( !safe_square(base) )
return std::nullopt;
}
return power;
}
template< class Ux, class Un>
auto nth_root(Ux x, Un n)
-> std::enable_if_t< std::is_unsigned_v<Ux> && std::is_unsigned_v<Un>
, Ux >
{
if ( n <= 1 ) {
if ( n < 1 ) {
std::cerr << "Domain error.\n";
return 0;
}
return x;
}
if ( x <= 1 )
return x;
std::uintmax_t nth_root = std::floor(std::pow(x, std::nextafter(1.0 / n, 1)));
// Rounding errors and overflows are possible
auto test = safe_pow(nth_root, n);
if (!test || test.value() > x )
return nth_root - 1;
test = safe_pow(nth_root + 1, n);
if ( test && test.value() <= x ) {
return nth_root + 1;
}
return nth_root;
}
constexpr inline size_t lowest_base{ 2 };
template <class N, class D = N>
auto to_digits( N n, D base )
{
std::vector<D> digits;
while ( n ) {
digits.push_back(n % base);
n /= base;
}
if (digits.empty())
digits.push_back(D{});
return digits;
}
template< class T >
T find_minimum_base(std::vector<T> const& digits)
{
assert( digits.size() );
return std::max( lowest_base
, digits.size() > 1
? *std::max_element(digits.cbegin(), digits.cend()) + 1
: digits.back() + 1);
}
template< class U, class Compare >
auto find_root(U low, Compare cmp) -> std::optional<U>
{
U high { low }, z{ low };
int result{};
while( (result = cmp(high)) < 0 ) {
z = high;
high *= 2;
}
if ( result == 0 ) {
return z;
}
low = z;
while ( low + 1 < high ) {
z = low + (high - low) / 2;
result = cmp(z);
if ( result == 0 ) {
return z;
}
if ( result < 0 )
low = z;
else if ( result > 0 )
high = z;
}
return std::nullopt;
}
namespace {
template< class NumberType > struct param_t
{
NumberType P, Q;
bool opposite_signs{};
public:
template< class Pt, class Qt >
param_t(Pt p, Qt q) : P{::num::abs(p)}, Q{::num::abs(q)}
{
if constexpr ( std::is_signed_v<Pt> )
opposite_signs = p < 0;
if constexpr ( std::is_signed_v<Qt> )
opposite_signs = opposite_signs != q < 0;
}
};
template< class NumberType > struct results_t
{
std::vector<NumberType> valid_bases;
bool has_infinite_results{};
};
template< class T >
std::ostream& operator<< (std::ostream& os, results_t<T> const& r)
{
if ( r.valid_bases.empty() )
os << "None.";
else if ( r.has_infinite_results )
os << "All the bases starting from " << r.valid_bases.back() << '.';
else {
for ( auto i : r.valid_bases )
os << i << ' ';
}
return os;
}
struct prime_factors_t
{
size_t factor, count;
};
} // End of unnamed namespace
auto prime_factorization(size_t n)
{
std::vector<prime_factors_t> factors;
size_t i = 2;
if (n % i == 0) {
size_t count = 0;
while (n % i == 0) {
n /= i;
count += 1;
}
factors.push_back({i, count});
}
for (size_t i = 3; i * i <= n; i += 2) {
if (n % i == 0) {
size_t count = 0;
while (n % i == 0) {
n /= i;
count += 1;
}
factors.push_back({i, count});
}
}
if (n > 1) {
factors.push_back({n, 1ull});
}
return factors;
}
auto prime_factorization_limited(size_t n, size_t max)
{
std::vector<prime_factors_t> factors;
size_t i = 2;
if (n % i == 0) {
size_t count = 0;
while (n % i == 0) {
n /= i;
count += 1;
}
factors.push_back({i, count});
}
for (size_t i = 3; i * i <= n && i <= max; i += 2) {
if (n % i == 0) {
size_t count = 0;
while (n % i == 0) {
n /= i;
count += 1;
}
factors.push_back({i, count});
}
}
if (n > 1 && n <= max) {
factors.push_back({n, 1ull});
}
return factors;
}
template< class F >
void apply_to_all_divisors( std::vector<prime_factors_t> const& factors
, size_t low, size_t high
, size_t index, size_t divisor, F use )
{
if ( divisor > high )
return;
if ( index == factors.size() ) {
if ( divisor >= low )
use(divisor);
return;
}
for ( size_t i{}; i <= factors[index].count; ++i) {
apply_to_all_divisors(factors, low, high, index + 1, divisor, use);
divisor *= factors[index].factor;
}
}
class ValidBases
{
using number_t = std::uintmax_t;
using digits_t = std::vector<number_t>;
param_t<number_t> param_;
digits_t Qs_;
results_t<number_t> results_;
public:
template< class Pt, class Qt >
ValidBases(Pt p, Qt q)
: param_{p, q}
{
Qs_ = to_digits(param_.Q, number_t{10});
search_bases();
}
auto& operator() () const { return results_; }
private:
void search_bases();
bool is_valid( number_t candidate );
int compare( number_t candidate );
};
void ValidBases::search_bases()
{
if ( param_.opposite_signs )
return;
if ( param_.P < Qs_[0] )
return;
number_t low = find_minimum_base(Qs_);
if ( param_.P == Qs_[0] ) {
results_.valid_bases.push_back(low);
results_.has_infinite_results = true;
return;
}
number_t P_ = param_.P - Qs_[0];
auto add_if_valid = [this](number_t x) mutable {
if ( is_valid(x) )
results_.valid_bases.push_back(x);
};
if ( Qs_.size() <= 2 ) {
auto factors = prime_factorization(P_);
apply_to_all_divisors(factors, low, P_, 0, 1, add_if_valid);
std::sort(results_.valid_bases.begin(), results_.valid_bases.end());
}
else {
number_t lim = std::max( nth_root(param_.P, Qs_.size())
, lowest_base );
auto factors = prime_factorization_limited(P_, lim);
apply_to_all_divisors(factors, low, lim, 0, 1, add_if_valid);
auto cmp = [this](number_t x) {
return compare(x);
};
auto b = find_root(lim + 1, cmp);
if ( b )
results_.valid_bases.push_back(b.value());
}
}
// Called only when P % candidate == Qs[0]
bool ValidBases::is_valid( number_t candidate )
{
size_t p = param_.P;
auto it = Qs_.cbegin();
while ( ++it != Qs_.cend() ) {
p /= candidate;
if ( p % candidate != *it )
return false;
}
return true;
}
int ValidBases::compare( number_t candidate )
{
auto Ps = to_digits(param_.P, candidate);
if ( Ps.size() < Qs_.size() )
return 1;
auto [ip, iq] = std::mismatch( Ps.crbegin(), Ps.crend()
, Qs_.crbegin());
if ( iq == Qs_.crend() )
return 0;
if ( *ip < *iq )
return 1;
return -1;
}
} // End of namespace 'num'
int main()
{
using Bases = num::ValidBases;
std::vector<std::pair<int, int>> tests {
{0,0}, {9, 9}, {3, 4}, {4, 0}, {4, 2}, {71, -4}, {71, 3}, {-71, -13},
{36, 100}, {172448, 12}, {172443, 123}
};
std::cout << std::setw(22) << "P" << std::setw(12) << "Q"
<< " valid bases\n\n";
for (auto sample : tests) {
auto [P, Q] = sample;
Bases a(P, Q);
std::cout << std::setw(22) << P << std::setw(12) << Q
<< " " << a() << '\n';
}
std::vector<std::pair<size_t, size_t>> tests_2 {
{49*25*8*81*11*17, 120}, {4894432871088700845ull, 13}, {18401055938125660803ull, 13},
{9249004726666694188ull, 19}, {18446744073709551551ull, 11}
};
for (auto sample : tests_2) {
auto [P, Q] = sample;
Bases a(P, Q);
std::cout << std::setw(22) << P << std::setw(12) << Q
<< " " << a() << '\n';
}
}
Testable here. Example of output:
P Q valid bases 0 0 All the bases starting from 2. 9 9 All the bases starting from 10. 3 4 None. 4 0 2 4 4 2 None. 71 -4 None. 71 3 4 17 34 68 -71 -13 4 68 36 100 3 2 6 172448 12 6 172446 172443 123 4 148440600 120 4 4894432871088700845 13 6 42 2212336518 4894432871088700842 18401055938125660803 13 13 17 23 18401055938125660800 9249004726666694188 19 9249004726666694179 18446744073709551551 11 2 18446744073709551550
To avoid the corner case P < 10
and P == Q
having an infinity of bases solution, I'll assume you are only interested in bases B <= P
.
Note that to have the last digit with the right value, you need P % B == Q % 10
which is equivalent to
B divides P - (Q % 10)
Let's use this fact to have a something more efficient.
#include <vector>
std::vector<size_t> find_divisors(size_t P) {
// returns divisors d of P, with 1 < d <= P
std::vector<size_t> D{P};
for(size_t i = 2; i <= P/i; ++i)
if (P % i == 0) {
D.push_back(i);
D.push_back(P/i);
}
return D;
}
std::vector<size_t> find_bases(size_t P, size_t Q) {
std::vector<size_t> bases;
for(size_t B: find_divisors(P - (Q % 10))) {
size_t p = P, q = Q;
while (q) {
if ((p % B) != (q % 10)) // checks digits are the same
break;
p /= B;
q /= 10;
}
if (q == 0) // all digits were equal
bases.push_back(B);
}
return bases;
}
#include <cstdio>
int main(int argc, char *argv[]) {
size_t P, Q;
sscanf(argv[1], "%zu", &P);
sscanf(argv[2], "%zu", &Q);
for(size_t B: find_bases(P, Q))
printf("%zu\n", B);
return 0;
}
The complexity is the same as finding all divisors of P - (Q%10)
, but you can't expect better, since if Q
is a single digit, those are exactly the solutions.
Small benchmark:
> time ./find_bases 16285263 13
12
4035
16285260
0.00s user 0.00s system 54% cpu 0.005 total
Bigger numbers:
> time ./find_bases 4894432871088700845 13
6
42
2212336518
4894432871088700842
25.80s user 0.04s system 99% cpu 25.867 total
And following, with a more complicated but faster implementation to find all divisors of 64 bits numbers.
#include <cstdio>
#include <map>
#include <numeric>
#include <vector>
std::vector<size_t> find_divisors(size_t P) {
// returns divisors d of P, with 1 < d <= P
std::vector<size_t> D{P};
for(size_t i = 2; i <= P/i; ++i)
if (P % i == 0) {
D.push_back(i);
D.push_back(P/i);
}
return D;
}
size_t mulmod(size_t a, size_t b, size_t mod) {
return (__uint128_t)a * b % mod;
}
size_t modexp(size_t base, size_t exponent, size_t mod)
{
size_t x = 1, y = base;
while (exponent) {
if (exponent & 1)
x = mulmod(x, y, mod);
y = mulmod(y, y, mod);
exponent >>= 1;
}
return x % mod;
}
bool deterministic_isprime(size_t p)
{
static const unsigned char bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
// https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases
if (p < 2)
return false;
if (p != 2 && p % 2 == 0)
return false;
size_t s = (p - 1) >> __builtin_ctz(p-1);
for (size_t i = 0; i < sizeof(bases); i++) {
size_t a = bases[i], temp = s;
size_t mod = modexp(a, temp, p);
while (temp != p - 1 && mod != 1 && mod != p - 1) {
mod = mulmod(mod, mod, p);
temp *= 2;
}
if (mod != p - 1 && temp % 2 == 0)
return false;
}
return true;
}
size_t abs_diff(size_t x, size_t y) {
return (x > y) ? (x - y) : (y - x);
}
size_t pollard_rho(size_t n, size_t x0=2, size_t c=1) {
auto f = [n,c](size_t x){ return (mulmod(x, x, n) + c) % n; };
size_t x = x0, y = x0, g = 1;
while (g == 1) {
x = f(x);
y = f(f(y));
g = std::gcd(abs_diff(x, y), n);
}
return g;
}
std::vector<std::pair<size_t, size_t>> factorize_small(size_t &P) {
std::vector<std::pair<size_t, size_t>> factors;
if ((P & 1) == 0) {
size_t ctz = __builtin_ctzll(P);
P >>= ctz;
factors.emplace_back(2, ctz);
}
size_t i;
for(i = 3; i <= P/i; i += 2) {
if (i > (1<<22))
break;
size_t multiplicity = 0;
while ((P % i) == 0) {
++multiplicity;
P /= i;
}
if (multiplicity)
factors.emplace_back(i, multiplicity);
}
if (P > 1 && i > P/i) {
factors.emplace_back(P, 1);
P = 1;
}
return factors;
}
std::vector<std::pair<size_t, size_t>> factorize_big(size_t P) {
auto factors = factorize_small(P);
if (P == 1)
return factors;
if (deterministic_isprime(P)) {
factors.emplace_back(P, 1);
return factors;
}
std::map<size_t, size_t> factors_map;
factors_map.insert(factors.begin(), factors.end());
size_t some_factor = pollard_rho(P);
for(auto i: {some_factor, P/some_factor})
for(auto const& [p, expo]: factorize_big(i))
factors_map[p] += expo;
return {factors_map.begin(), factors_map.end()};
}
std::vector<size_t> all_divisors(size_t P) {
std::vector<size_t> divisors{1};
for(auto const& [p, expo]: factorize_big(P)) {
size_t ppow = p, previous_size = divisors.size();
for(size_t i = 0; i < expo; ++i, ppow *= p)
for(size_t j = 0; j < previous_size; ++j)
divisors.push_back(divisors[j] * ppow);
}
return divisors;
}
std::vector<size_t> find_bases(size_t P, size_t Q) {
if (P <= (Q%10))
return {};
std::vector<size_t> bases;
for(size_t B: all_divisors(P - (Q % 10))) {
if (B == 1)
continue;
size_t p = P, q = Q;
while (q) {
if ((p % B) != (q % 10)) // checks digits are the same
break;
p /= B;
q /= 10;
}
if (q == 0) // all digits were equal
bases.push_back(B);
}
return bases;
}
int main(int argc, char *argv[]) {
std::vector<std::pair<size_t, size_t>> tests;
if (argc > 1) {
size_t P, Q;
sscanf(argv[1], "%zu", &P);
sscanf(argv[2], "%zu", &Q);
tests.emplace_back(P, Q);
} else {
tests.assign({
{0,0}, {9, 9}, {3, 4}, {4, 0}, {4, 2}, {71, 3}, {71, 13},
{36, 100}, {172448, 12}, {172443, 123},
{49*25*8*81*11*17, 120}, {4894432871088700845ull, 13}, {18401055938125660803ull, 13},
{9249004726666694188ull, 19}
});
}
for(auto & [P, Q]: tests) {
auto bases = find_bases(P, Q);
if (tests.size() > 1)
printf("%zu, %zu: ", P, Q);
if (bases.empty()) {
printf(" None");
} else {
for(size_t B: bases)
printf("%zu ", B);
}
printf("\n");
}
return 0;
}
We now have:
> time ./find_bases
0, 0: None
9, 9: None
3, 4: None
4, 0: 2 4
4, 2: None
71, 3: 4 17 34 68
71, 13: 4 68
36, 100: 2 3 6
172448, 12: 6 172446
172443, 123: 4
148440600, 120: 4
4894432871088700845, 13: 6 42 2212336518 4894432871088700842
18401055938125660803, 13: 13 17 23 18401055938125660800
9249004726666694188, 19: 9249004726666694179 9249004726666694179
0.09s user 0.00s system 96% cpu 0.093 total
Fast as can be :)
(NB: this would be around 10 seconds with the answer from Bob__ )