# Sieving Multiplicative Functions

Posted April 15, 2026

The sieve of Eratosthenes is a well-known algorithm for computing all the prime numbers up to a given bound. There are also well-known variants on the sieve. In particular, a segmented version can be implemented to limit memory usage.

Furthermore, a similar approach can be used to "sieve" the values of multiplicative functions. For instance, this page discusses computing values of Euler's totient function with an approach that results in the same time complexity as the standard sieve.

This Codeforces page also discusses an approach for sieving any multiplicative function. However, the provided code has some downsides: it's not segmented, and the modifications needed for a given multiplicative function are not always obvious.

Here, I'll discuss a variant of the sieve that

- is segmented,
- works for any multiplicative function,
- works with a multiplicative function's "prime power" definition, and
- has the same time complexity as the standard sieve (assuming the function has an efficient implementation for prime powers).

## Demo

Note that for large ranges, outputting the full result will chew up a lot of memory.

l (lower bound) r (upper bound) Function

## Multiplicative functions

Multiplicative functions are functions with a special property: if two numbers a and b are coprime (i.e. their greatest common divisor is 1), then f ( a b ) = f ( a ) f ( b ) . This means if we can factor a number n into coprime integers x and y, we can find f(n) by instead finding f(x) and f(y) and multiplying them.

Going a step further, if we have a prime factorization of a number n, along with a formula for f at prime powers, we can easily compute f(n) by evaluating f at each prime power and multiplying.

It turns out there are lots of multiplicative functions, including many important functions in number theory, such as:

- the divisor function
- Euler's totient function
- the Mobius function

Furthermore, many of these functions have very simple formulas for prime power inputs, making them easy to evaluate if you have a prime factorization.

### An example

Suppose we want to know how many divisors 120 has. We could try counting every divisor by hand, but that would be pretty tedious and error-prone. We could also write a simple loop to have a computer find the answer, but that approach wouldn't scale well for larger numbers. Instead, we'll take advantage of the fact that the number of divisors function is multiplicative to find the answer.

We'll use d(n) to denote the function. It has a convenient formula for prime powers:1

d ( p k ) = k + 1

This lets us answer our question by factoring 120:2

d ( 120 ) = d ( 2 3 × 3 × 5 ) = d ( 2 3 ) d ( 3 ) d ( 5 ) = 4 × 2 × 2 = 16

Sure enough, the 16 divisors are 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 24, 30, 40, 60, and 120.

Suppose, however, that we want to find the number of divisors for each number up to 120. There's no general algorithm to efficiently factor a number, so it isn't a great idea to factor every number to get the solution. We'll take a different approach, but it will still take advantage of the multiplicative property.

### What about additive functions?

There's another class of functions called additive functions with similar properties to multiplicative functions, namely that f ( a b ) = f ( a ) + f ( b ) whenever a and b are coprime. We can sieve these functions in nearly the exact same manner as multiplicative functions (e.g. the prime omega functions in the demo are additive). When we get to the code, you'll see the provided functions take a boolean indicating whether the function is additive to apply the proper logic.

## The algorithm

Here's the routine for sieving multiplicative functions:

    function sieveSegment(l, r, f, primes, additive) {
    	const [sieve, quo] = initArrays(l, r, additive);

    	for (let i = 0; i < primes.length; i++) {
    		if (i*i > r) {
    			break;
    		}

    		if (!primes[i]) {
    			continue;
    		}

    		sievePowers(i, l, r, f, sieve, quo, additive);
    	}

    	unusuals(f, sieve, quo, additive);

    	return sieve;
    }

This function performs a segmented sieve on any multiplicative (or additive) function.

`l` and `r` are the lower and upper bounds for the sieve, respectively. `f` is the function we are sieving. `primes` is an array indicating all the prime numbers up to the square root of `r`, which we can precompute with the standard sieve. Finally, `additive` is a boolean indicating if the function is additive or multiplicative.

The first step of the function is to initialize two arrays of size `r - l` called `sieve` and `quo`. `sieve` is what ultimately gets returned and stores the values of the function we're sieving. `quo` is an internal array we use to finalize the values of sieve at the very end of the function.

    function initArrays(l, r, additive) {
    	const sieve = Array(r - l);
    	const quo = Array(r - l);
    	for (let i = 0; i < r-l; i++) {
    		quo[i] = l + i;
    		if (additive) {
    			sieve[i] = 0;
    		} else {
    			sieve[i] = 1;
    		}
    	}

    	return [sieve, quo];
    }

The `initArrays` function initializes arrays that keep track of the value of the function we're sieving and the running quotients.

Say we want to calculate the number of divisors function from 30 to 60. After `initArrays` runs, here's what the `sieve` and `quo` arrays look like:

Initial values of `sieve` and `quo`
n | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59
`sieve` | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
`quo` | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59

Next, we'll call the following functions for every prime number in the `primes` array. This is where the function spends most of its time.

    function sievePowers(p, l, r, f, sieve, quo, additive) {
    	let i = 1;
    	for (let q = p; q < r; q *= p) {
    		const y = f(p, i);
    		sieveMultiples(p, q, l, r, y, sieve, quo, additive);
    		i++;
    	}
    }

    function sieveMultiples(p, q, l, r, y, sieve, quo, additive) {
    	const low = Math.ceil(l / q);
    	for (let i = low * q; i < r; i += q) {
    		if (i % (p*q) === 0) {
    			continue;
    		}

    		if (additive) {
    			sieve[i - l] += y;
    		} else {
    			sieve[i - l] *= y;
    		}

    		quo[i - l] /= q
    	}
    }

These functions are where most of the computation happens.

In short, these functions evaluate our function at prime powers only, then update the values in the `sieve` array for non-prime powers using the multiplicative property. We also update the `quo` array by dividing out the factors we find along the way.

Here's what the arrays will look like after sieving with 2:

Values of `sieve` and `quo` after sieving 2
n | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59
`sieve` | 2 | 1 | 6 | 1 | 2 | 1 | 3 | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 3 | 1 | 2 | 1 | 5 | 1 | 2 | 1 | 3 | 1 | 2 | 1 | 4 | 1 | 2 | 1
`quo` | 15 | 31 | 1 | 33 | 17 | 35 | 9 | 37 | 19 | 39 | 5 | 41 | 21 | 43 | 11 | 45 | 23 | 47 | 3 | 49 | 25 | 51 | 13 | 53 | 27 | 55 | 7 | 57 | 29 | 59

And here's what the arrays will look like after subsequently sieving with 3:

Values of `sieve` and `quo` after sieving 3
n | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59
`sieve` | 4 | 1 | 6 | 2 | 2 | 1 | 9 | 1 | 2 | 2 | 4 | 1 | 4 | 1 | 3 | 3 | 2 | 1 | 10 | 1 | 2 | 2 | 3 | 1 | 8 | 1 | 4 | 2 | 2 | 1
`quo` | 5 | 31 | 1 | 11 | 17 | 35 | 1 | 37 | 19 | 13 | 5 | 41 | 7 | 43 | 11 | 5 | 23 | 47 | 1 | 49 | 25 | 17 | 13 | 53 | 1 | 55 | 7 | 19 | 29 | 59

After we've gone through the entire `primes` array, the two arrays will look like this:

Values of `sieve` and `quo` just before calling `unusuals`
n | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59
`sieve` | 8 | 1 | 6 | 2 | 2 | 4 | 9 | 1 | 2 | 2 | 8 | 1 | 8 | 1 | 3 | 6 | 2 | 1 | 10 | 3 | 6 | 2 | 3 | 1 | 8 | 2 | 8 | 2 | 2 | 1
`quo` | 1 | 31 | 1 | 11 | 17 | 1 | 1 | 37 | 19 | 13 | 1 | 41 | 1 | 43 | 11 | 1 | 23 | 47 | 1 | 1 | 1 | 17 | 13 | 53 | 1 | 11 | 1 | 19 | 29 | 59

At this point, there's one last step. Numbers can have up to one prime factor greater than their own square root. When that happens, they're called unusual numbers.3 But since we only require the `primes` array to hold primes up to the square root of `r`, any unusual number will have one last prime factor that we never evaluated. Fortunately, we can identify those numbers easily using the `quo` array, so we just need to do one more pass through that array to finalize everything.

    function unusuals(f, sieve, quo, additive) {
    	for (let i = 0; i < quo.length; i++) {
    		const q = quo[i];
    		if (q === 1) {
    			continue;
    		}

    		if (additive) {
    			sieve[i] += f(q, 1);
    		} else {
    			sieve[i] *= f(q, 1);
    		}
    	}
    }

The last step is to finalize the values of any unusual numbers (numbers n with a prime factor greater than sqrt(n)).

### Analysis

It's pretty easy to see that `initArrays` and `unusuals` are O(r-l).4 Analyzing `sievePowers` and its impact is a bit more involved.

Looking at `sieveMultiples`, its main loop performs a fixed number of arithmetic operations O ( r − l q ) times. `sieveMultiples`, in turn, is called by `sievePowers` at values of q=p, q=p 2, q=p 3, and so on until pk exceeds r. This means the total time complexity of `sievePowers` is4

O ( r − l p + r − l p 2 + r − l p 3 + … ) = O ( r − l p )

With this in mind, we can evaluate `sieveSegment`, which calls `sievePowers` for every prime less than the square root of `r`. Thanks to Euler, we know that the sum of the reciprocal of primes up to k is asymptotic to log ⁡ log ⁡ k . Therefore, the time complexity of `sieveSegment` is

O ( r − l 2 + r − l 3 + r − l 5 + … ) = O ( ( r − l ) log ⁡ log ⁡ r ) = O ( ( r − l ) log ⁡ log ⁡ r )

This is the same complexity as the method given here for sieving the totient function.

## Final thoughts

This all ends up being more lines of code than writing a single sieve for, say, totient or the number of divisors. However, if you want to sieve multiple functions, it can be tricky to make the right modifications to an existing sieve to work with a different function.

In contrast, with this approach, you can use the same routine and simply pass a different function as an argument. For example, here's all the functions available in the above demo:

    function numDivisors(p, k) {
    	return k + 1;
    }

    function sumDivisors(p, k) {
    	return (p**(k+1) - 1) / (p - 1)
    }

    function totient(p, k) {
    	return p**(k-1) * (p - 1);
    }

    function radical(p, k) {
    	return p;
    }

    function littleOmega(p, k) {
    	return 1;
    }

    function bigOmega(p, k) {
    	return k;
    }

    function mobius(p, k) {
    	if (k === 1) {
    		return -1;
    	}

    	return 0;
    }

Functions that can be sieved in the demo.

Many multiplicative and additive functions have simple formulas for prime powers, which makes adding a new function to sieve very easy with this approach.

## Footnotes

1. 1 This is because the only numbers that divide pk are 1, p, p 2, ..., up to pk, which is k+1 total numbers.
2. 2 You might be thinking at this point that factoring is also not very efficient. Sit tight; this is only an example to get you comfortable with multiplicative functions. We're going to take advantage of this property later.
3. 3 "Unusual" is actually a bit of a misnomer - most numbers have a prime factor greater than their square root, so unusual numbers are actually more common than so-called "usual" numbers!
4. 4 Well, technically `unusuals` and `sievePowers` are also dependent on the time complexity of the function we're sieving, but generally that complexity isn't too bad, so I'm going to gloss over that bit.