Efficient way to find divisibility

Viewed 1279

Professor says this isn't a efficient algorithm to check whether the number is divisible by a number from 100,000-150,000. I'm having trouble finding a better way. Any help would be appreciated.

unsigned short divisibility_check(unsigned long n) {
    unsigned long i;
    for (i = 100000; i <= 150000; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}
5 Answers

Let's say you need to find whether a positive integer K is divisible by a number between 100,000 and 150,000, and it is such a rare operation, that doing precalculations is just not worth the processor time or memory used.

If K < 100,000, it cannot be divisible by a number between 100,000 and 150,000.

If 100,000 ≤ K ≤ 150,000, it is divisible by itself. It is up to you to decide whether this counts or not.

For a K > 150,000 to be divisible by M, with 100,000 ≤ M ≤ 150,000, K must also be divisible by L = K / M. This is because K = L × M, and all three are positive integers. So, you only need to test the divisibility of K by a set of L, where ⌊ K / 150,000 ⌋ ≤ L ≤ ⌊ K / 100,000 ⌋.

However, that set of Ls becomes larger than the set of possible Ms when K > = 15,000,000,000. Then it is again less work to just test K for divisibility against each M, much like OP's code is now.


When implementing this as a program, the most important thing in practice is, surprisingly, the comments you add. Do not write comments that describe what the code does; write comments that explain the model or algorithm you are trying to implement (say, at the function level), and your intent of what each small block of code should accomplish.

In this particular case, you should probably add a comment to each if clause, explaining your reasoning, much like I did above.

Beginner programmers often omit comments completely. It is unfortunate, because writing good comments is a hard habit to pick up afterwards. It is definitely a good idea to learn to comment your code (as I described above -- the comments that describe what the code does are less than useful; more noise than help), and keep honing your skill on that.

A programmer whose code is maintainable, is worth ten geniuses who produce write-only code. This is because all code has bugs, because humans make errors. To be an efficient developer, your code must be maintainable. Otherwise you're forced to rewrite each buggy part from scratch, wasting a lot of time. And, as you can see above, "optimization" at the algorithmic level, i.e. thinking about how to avoid having to do work, yields much better results than trying to optimize your loops or something like that. (You'll find in real life that surprisingly often, optimizing a loop in the proper way, removes the loop completely.)

Even in exercises, proper comments may be the difference between "no points, this doesn't work" and "okay, I'll give you partial credit for this one, because you had a typo/off-by-one bug/thinko on line N, but otherwise your solution would have worked".


As bolov did not understand how the above leads to a "naive_with_checks" function, I'll show it implemented here.

For ease of testing, I'll show a complete test program. Supply the range of integers to test, and the range of divisors accepted, as parameters to the program (i.e. thisprogram 1 500000 100000 150000 to duplicate bolov's tests).

#include <stdlib.h>
#include <inttypes.h>
#include <limits.h>
#include <locale.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>

int is_divisible(const uint64_t  number,
                 const uint64_t  minimum_divisor,
                 const uint64_t  maximum_divisor)
{
    uint64_t divisor, minimum_result, maximum_result, result;

    if (number < minimum_divisor) {
        return 0;
    }

    if (number <= maximum_divisor) {
        /* Number itself is a valid divisor. */
        return 1;
    }

    minimum_result = number / maximum_divisor;
    if (minimum_result < 2) {
        minimum_result = 2;
    }

    maximum_result = number / minimum_divisor;
    if (maximum_result < minimum_result) {
        maximum_result = minimum_result;
    }

    if (maximum_result - minimum_result > maximum_divisor - minimum_divisor) {
        /* The number is so large that it is the least amount of work
           to check each possible divisor. */
        for (divisor = minimum_divisor; divisor <= maximum_divisor; divisor++) {
            if (number % divisor == 0) {
                return 1;
            }
        }

        return 0;

    } else {
        /* There are fewer possible results than divisors,
           so we check the results instead. */

        for (result = minimum_result; result <= maximum_result; result++) {
            if (number % result == 0) {
                divisor = number / result;
                if (divisor >= minimum_divisor && divisor <= maximum_divisor) {
                    return 1;
                }
            }
        }

        return 0;
    }
}

int parse_u64(const char *s, uint64_t *to)
{
    unsigned long long  value;
    const char         *end;

    /* Empty strings are not valid. */
    if (s == NULL || *s == '\0')
        return -1;

    /* Parse as unsigned long long. */
    end = s;
    errno = 0;
    value = strtoull(s, (char **)(&end), 0);
    if (errno == ERANGE)
        return -1;
    if (end == s)
        return -1;

    /* Overflow? */
    if (value > UINT64_MAX)
        return -1;

    /* Skip trailing whitespace. */
    while (isspace((unsigned char)(*end)))
        end++;

    /* If the string does not end here, it has garbage in it. */
    if (*end != '\0')
        return -1;

    if (to)
        *to = (uint64_t)value;

    return 0;
}

int main(int argc, char *argv[])
{
    uint64_t kmin, kmax, dmin, dmax, k, count;

    if (argc != 5) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help | help ]\n", argv[0]);
        fprintf(stderr, "       %s MIN MAX MIN_DIVISOR MAX_DIVISOR\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program counts which positive integers between MIN and MAX,\n");
        fprintf(stderr, "inclusive, are divisible by MIN_DIVISOR to MAX_DIVISOR, inclusive.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    /* Use current locale. This may change which codes isspace() considers whitespace. */
    if (setlocale(LC_ALL, "") == NULL)
        fprintf(stderr, "Warning: Your C library does not support your current locale.\n");

    if (parse_u64(argv[1], &kmin) || kmin < 1) {
        fprintf(stderr, "%s: Invalid minimum positive integer to test.\n", argv[1]);
        return EXIT_FAILURE;
    }
    if (parse_u64(argv[2], &kmax) || kmax < kmin || kmax >= UINT64_MAX) {
        fprintf(stderr, "%s: Invalid maximum positive integer to test.\n", argv[2]);
        return EXIT_FAILURE;
    }
    if (parse_u64(argv[3], &dmin) || dmin < 2) {
        fprintf(stderr, "%s: Invalid minimum divisor to test for.\n", argv[3]);
        return EXIT_FAILURE;
    }
    if (parse_u64(argv[4], &dmax) || dmax < dmin) {
        fprintf(stderr, "%s: Invalid maximum divisor to test for.\n", argv[4]);
        return EXIT_FAILURE;
    }

    count = 0;
    for (k = kmin; k <= kmax; k++)
        count += is_divisible(k, dmin, dmax);

    printf("%" PRIu64 "\n", count);
    return EXIT_SUCCESS;
}

It is useful to note that the above, running bolov's test, i.e. thisprogram 1 500000 100000 150000 only takes about 15 ms of wall clock time (13 ms CPU time), median, on a much slower Core i5-7200U processor. For really large numbers, like 280,000,000,000 to 280,000,010,000, the test does the maximum amount of work, and takes about 3.5 seconds per 10,000 numbers on this machine.

In other words, I wouldn't trust bolov's numbers to have any relation to timings for properly written test cases.

It is important to note that for any K between 1 and 500,000, the same test that bolov says their code measures, the above code does at most two divisibility tests to find if K is divisible by an integer between 100,000 and 150,000.

This solution is therefore quite efficient. It is definitely acceptable and near-optimal, when the tested K are relatively small (say, 32 bit unsigned integers or smaller), or when precomputed tables cannot be used.

Even when precomputed tables can be used, it is unclear if/when prime factorization becomes faster than the direct checks. There is certainly a tradeoff in the size and content of the precomputed tables. bolov claims that it is clearly superior to other methods, but hasn't implemented a proper "naive" divisibility test as shown above, and bases their opinion on experiments on quite small integers (1 to 500,000) that have simple prime decompositions.

As an example, a table of integers 1 to 500,000 pre-checked for divisibility takes only 62500 bytes (43750 bytes for 150,000 to 500,000). With that table, each test takes a small near-constant time (that only depends on memory and cache effects). Extending it to all 32-bit unsigned integers would require 512 GiB (536,870,912 bytes); the table can be stored in a memory-mapped read-only file, to let the OS kernel manage how much of it is mapped to RAM at any time.

Prime decomposition itself, especially using trial division, becomes more expensive than the naive approach when the number of trial divisions exceeds the range of possible divisors (50,000 divisors in this particular case). As there are 13848 primes (if one counts 1 and 2 as primes) between 1 and 150,000, the number of trial divisions can easily approach the number of divisors for sufficiently large input values.

For numbers with many prime factors, the combinatoric phase, finding if any subset of the prime factors multiply to a number between 100,000 and 150,000 is even more problematic. The number of possible combinations grows faster than exponentially. Without careful checks, this phase alone can do way more work per large input number than just trial division with each possible divisor would be.

(As an example, if you have 16 different prime factors, you already have 65,535 different combinations; more than the number of direct trial divisions. However, all such numbers are larger than 64-bit; the smallest being 2·3·5·7·11·13·17·19·23·29·31·37·41·43·47·53 = 32,589,158,477,190,044,730 which is a 65-bit number.)

There is also the problem of code complexity. The more complex the code, the harder it is to debug and maintain.

Ok, so I've implemented the version with sieve primes and factorization mentioned in the comments by m69 and it is ... way faster than the naive approach. I must admit, I didn't expect this at all.

My notations: left == 100'000 and right = 150'000

  • naive your version
  • naive_with_checks your version with simple checks:

    • if (n < left) no divisor
    • else if (n <= right) divisor
    • else if (left * 2 >= right && n < left * 2) divisor
  • factorization (above checks implemented)

    1. Precompute the Sieve of Eratosthenes for all primes up to right. This time is not measured
    2. factorize n (only with the primes from the prev step)
    3. generate all subsets (backtracking, depth first: i.e. generate p1^0 * p2^0 * p3^0 first, instead of p1^5 first) with the product < left or until the product is in [left, right] (found divisor).
  • factorization_opt optimization of the previous algorithm where the subsets are not generated (no vector of subsets is created). I just pass the current product from one backtracking iteration to the next.

  • Nominal Animal's version I have also ran his version on my system with the same range.

I have written the program in C++ so I won't share it here.

I used std::uint64_t as data type and I have checked all numbers from 1 to 500'000 to see if each is divisible by a number in interval [100'000, 150'000]. All version reached the same solution: 170'836 numbers with positive results.

The setup:

  • Hardware: Intel Core i7-920, 4 cores with HT (all algorithm versions are single threaded), 2.66 GHz (boost 2.93 GHz), 8 MB SmartCache; memory: 6 GB DDR3 triple channel.

  • Compiler: Visual Studio 2017 (v141), Release x64 mode.

I must also add that I haven't profiled the programs so there is definitely room to improve the implementation. However this is enough here as the idea is to find a better algorithm.

version                |  elapsed time (milliseconds)
-----------------------+--------------
naive                  |  167'378 ms (yes, it's thousands separator, aka 167 seconds)
naive_with_checks      |   97'197 ms
factorization          |    7'906 ms
factorization_opt      |    7'320 ms
                       |
Nominal Animal version |       14 ms

Some analysis:

For naive vs naive_with_checks: all the numbers in [1 200'000] can be solved with just the simple checks. As these represent 40% of all the numbers checked, the naive_with_checks version does roughly 60% of the work naive does. The execution time reflect this as naive_with_checks runtime is ≅58% of the naive version.

The factorization version is a whopping 12.3 times faster. That is indeed impressive. I haven't analyzed the time complexity of the alg.

And the final optimization brings a further 1.08x speedup. This is basically the time gained by removing the creation and copy of the small vectors of subset factors.

For those interested the sieve precomputation which is not included above takes about 1 ms. And this is the naive implementation from wikipedia, no optimizations whatsoever.

Here's a recursive method with primes. The idea here is that if a number is divisible by a number between 100000 and 150000, there is a path of reducing by division the product of only relevant primes that will pass through a state in the target range. (Note: the code below is meant for numbers greater than 100000*150000). In my testing, I could not find an instance where the stack performed over 600 iterations.

# Euler sieve
def getPrimes():
  n = 150000
  a = (n+1) * [None]
  ps = ([],[])
  s = []
  p = 1

  while (p < n):
    p = p + 1

    if not a[p]:
      s.append(p)
      # Save primes less
      # than half
      # of 150000, the only
      # ones needed to construct
      # our candidates.
      if p < 75000:
        ps[0].append(p);
      # Save primes between
      # 100000 and 150000
      # in case our candidate
      # is prime.
      elif p > 100000:
        ps[1].append(p)

      limit = n / p
      new_s = []

      for i in s:
        j = i
        while j <= limit:
          new_s.append(j)
          a[j*p] = True
          j = j * p
      s = new_s

  return ps

ps1, ps2 = getPrimes()

def f(n):
  # Prime candidate
  for p in ps2:
    if not (n % p):
      return True

  # (primes, prime_counts)
  ds = ([],[])
  prod = 1
  # Prepare only prime
  # factors that could
  # construct a composite
  # candidate.
  for p in ps1:
    while not (n % p):
      prod *= p
      if (not ds[0] or ds[0][-1] != p):
        ds[0].append(p)
        ds[1].append(1)
      else:
        ds[1][-1] += 1
      n /= p

  # Reduce the primes product to
  # a state where it's between
  # our target range.
  stack = [(prod,0)]

  while stack:
    prod, i = stack.pop()

    # No point in reducing further
    if prod < 100000:
      continue
    # Exit early
    elif prod <= 150000:
      return True
    # Try reducing the product
    # by different prime powers
    # one prime at a time
    if i < len(ds[0]):
      for p in xrange(ds[1][i] + 1):
        stack.append((prod / ds[0][i]**p, i + 1))

  return False

Output:

c = 0

for ii in xrange(1099511627776, 1099511628776):
  f_i = f(ii)

  if f_i:
    c += 1

print c # 239

For comparison, here's what I had in mind when I posted my comment about using prime factorization. Compiled with gcc -std=c99 -O3 -m64 -march=haswell this is slightly faster than the naive method with checks and inversion when tested with the last 10,000 integers in the 64-bit range (3.469 vs 3.624 seconds).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>

void eratosthenes(bool *ptr, uint64_t size) {
    memset(ptr, true, size);
    for (uint64_t i = 2; i * i < size; i++) {
        if (ptr[i]) {
            for (uint64_t j = i * i; j < size; j += i) {
                ptr[j] = false;
            }
        }
    }
}

bool divisible(uint64_t n, uint64_t a, uint64_t b) {
    /* check for trivial cases first                                    */
    if (n < a) {
        return false;
    }
    if (n <= b) {
        return true;
    }
    if (n < 2 * a) {
        return false;
    }

    /* Inversion: use range n/b ~ n/a; see Nominal Animal's answer      */
    if (n < a * b) {
        uint64_t c = a;
        a = (n + b - 1) / b; // n/b rounded up
        b = n / c;
    }

    /* Create prime sieve when first called, or re-calculate it when    */
    /* called with a higher value of b; place before inversion in case  */
    /* of a large sequential test, to avoid repeated re-calculation.    */
    static bool *prime = NULL;
    static uint64_t prime_size = 0;
    if (prime_size <= b) {
        prime_size = b + 1;
        prime = realloc(prime, prime_size * sizeof(bool));
        if (!prime) {
            printf("Out of memory!\n");
            return false;
        }
        eratosthenes(prime, prime_size);
    }

    /* Factorize n into prime factors up to b, using trial division;    */
    /* there are more efficient but also more complex ways to do this.  */
    /* You could return here, if a factor in the range a~b is found.    */
    static uint64_t factor[63];
    uint8_t factors = 0;
    for (uint64_t i = 2; i <= n && i <= b; i++) {
        if (prime[i]) {
            while (n % i == 0) {
                factor[factors++] = i;
                n /= i;
            }
        }
    }

    /* Prepare divisor sieve when first called, or re-allocate it when  */
    /* called with a higher value of b; in a higher-level language, you */
    /* would probably use a different data structure for this, because  */
    /* this method iterates repeatedly over a potentially sparse array. */
    static bool *divisor = NULL;
    static uint64_t div_size = 0;
    if (div_size <= b / 2) {
        div_size = b / 2 + 1;
        divisor = realloc(divisor, div_size * sizeof(bool));
        if (!divisor) {
            printf("Out of memory!\n");
            return false;
        }
    }
    memset(divisor, false, div_size);
    divisor[1] = true;
    uint64_t max = 1;

    /* Iterate over each prime factor, and for every divisor already in */
    /* the sieve, add the product of the divisor and the factor, up to  */
    /* the value b/2. If the product is in the range a~b, return true.  */
    for (uint8_t i = 0; i < factors; i++) {
        for (uint64_t j = max; j > 0; j--) {
            if (divisor[j]) {
                uint64_t product = factor[i] * j;
                if (product >= a && product <= b) {
                    return true;
                }
                if (product < div_size) {
                    divisor[product] = true;
                    if (product > max) {
                        max = product;
                    }
                }
            }
        }
    }
    return false;
}

int main() {
    uint64_t count = 0;
    for (uint64_t n = 18446744073709541615LLU; n <= 18446744073709551614LLU; n++) {
        if (divisible(n, 100000, 150000)) ++count;
    }
    printf("%llu", count);
    return 0;
}

And this is the naive + checks + inversion implementation I compared it with:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

bool divisible(uint64_t n, uint64_t a, uint64_t b) {
    if (n < a) {
        return false;
    }
    if (n <= b) {
        return true;
    }
    if (n < 2 * a) {
        return false;
    }
    if (n < a * b) {
        uint64_t c = a;
        a = (n + b - 1) / b;
        b = n  / c;
    }
    while (a <= b) {
        if (n % a++ == 0) return true;
    }
    return false;
}

int main() {
    uint64_t count = 0;
    for (uint64_t n = 18446744073709541615LLU; n <= 18446744073709551614LLU; n++) {
        if (divisible(n, 100000, 150000)) ++count;
    }
    printf("%llu", count);
    return 0;
}

Here is a very simple solution with a sieve cache. If you call the divisibility_check function for many numbers in a sequence, this should be very efficient:

#include <string.h>

int divisibility_check_sieve(unsigned long n) {
    static unsigned long sieve_min = 1, sieve_max;
    static unsigned char sieve[1 << 19]; /* 1/2 megabyte */
    if (n < sieve_min || n > sieve_max) {
        sieve_min = n & ~(sizeof(sieve) - 1);
        sieve_max = sieve_min + sizeof(sieve) - 1;
        memset(sieve, 1, sizeof sieve);
        for (unsigned long m = 100000; m <= 150000; m++) {
            unsigned long i = sieve_min % m;
            if (i != 0)
                i = m - i;
            for (; i < sizeof sieve; i += m) {
                sieve[i] = 0;
            }
        }
    }
    return sieve[n - sieve_min];
}

Here is a comparative benchmark:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int divisibility_check_naive(unsigned long n) {
    for (unsigned long i = 100000; i <= 150000; i++) {
        if (n % i == 0) {
            return 0;
        }
    }
    return 1;
}

int divisibility_check_small(unsigned long n) {
    unsigned long i, min = n / 150000, max = n / 100000;
    min += (min == 0);
    max += (max == 0);
    if (max - min > 150000 - 100000) {
        for (i = 100000; i <= 150000; i++) {
            if (n % i == 0) {
                return 0;
            }
        }
        return 1;
    } else {
        for (i = min; i <= max; i++) {
            if (n % i == 0) {
                unsigned long div = n / i;
                if (div >= 100000 && div <= 150000)
                    return 0;
            }
        }
        return 1;
    }
}

int divisibility_check_sieve(unsigned long n) {
    static unsigned long sieve_min = 1, sieve_max;
    static unsigned char sieve[1 << 19]; /* 1/2 megabyte */
    if (n < sieve_min || n > sieve_max) {
        sieve_min = n & ~(sizeof(sieve) - 1);
        sieve_max = sieve_min + sizeof(sieve) - 1;
        memset(sieve, 1, sizeof sieve);
        for (unsigned long m = 100000; m <= 150000; m++) {
            unsigned long i = sieve_min % m;
            if (i != 0)
                i = m - i;
            for (; i < sizeof sieve; i += m) {
                sieve[i] = 0;
            }
        }
    }
    return sieve[n - sieve_min];
}

int main(int argc, char *argv[]) {
    unsigned long n, count = 0, lmin, lmax, range[2] = { 1, 500000 };
    int pos = 0, naive = 0, small = 0, sieve = 1;
    clock_t t;
    char *p;

    for (int i = 1; i < argc; i++) {
        n = strtoul(argv[i], &p, 0);
        if (*p == '\0' && pos < 2)
            range[pos++] = n;
        else if (!strcmp(argv[i], "naive"))
            naive = 1;
        else if (!strcmp(argv[i], "small"))
            small = 1;
        else if (!strcmp(argv[i], "sieve"))
            sieve = 1;
        else
            printf("invalid argument: %s\n", argv[i]);
    }
    lmin = range[0];
    lmax = range[1] + 1;
    if (naive) {
        t = clock();
        for (count = 0, n = lmin; n != lmax; n++) {
            count += divisibility_check_naive(n);
        }
        t = clock() - t;
        printf("naive: [%lu..%lu] -> %lu non-divisible numbers, %10.2fms\n",
               lmin, lmax - 1, count, t * 1000.0 / CLOCKS_PER_SEC);
    }
    if (small) {
        t = clock();
        for (count = 0, n = lmin; n != lmax; n++) {
            count += divisibility_check_small(n);
        }
        t = clock() - t;
        printf("small: [%lu..%lu] -> %lu non-divisible numbers, %10.2fms\n",
               lmin, lmax - 1, count, t * 1000.0 / CLOCKS_PER_SEC);
    }
    if (sieve) {
        t = clock();
        for (count = 0, n = lmin; n != lmax; n++) {
            count += divisibility_check_sieve(n);
        }
        t = clock() - t;
        printf("sieve: [%lu..%lu] -> %lu non-divisible numbers, %10.2fms\n",
               lmin, lmax - 1, count, t * 1000.0 / CLOCKS_PER_SEC);
    }
    return 0;
}

Here are some run times:

naive: [1..500000] -> 329164 non-divisible numbers,  158174.52ms
small: [1..500000] -> 329164 non-divisible numbers,      12.62ms
sieve: [1..500000] -> 329164 non-divisible numbers,       1.35ms
sieve: [0..4294967295] -> 3279784841 non-divisible numbers,    8787.23ms
sieve: [10000000000000000000..10000000001000000000] -> 765978176 non-divisible numbers,    2205.36ms
Related