Programming assignment #5: Computing large powers efficiently

Course links

External links

Computing large powers

The java.util.BigInteger class includes a method, pow, for raising BigInteger values to arbitrary non-negative powers. When the exponent is large, computing the result by iterated multiplication, as in

BigInteger result = BigInteger.ONE;
for (int step = 0; step < exponent; step++)
    result = result.multiply(base);

is unnecessarily slow. For most exponents, it is faster to combine some multiplications by the base with some squaring operations. For instance, to compute the 18th power of base, we might use a plan based on the following observations:

base18 = (base9)2
= (base8 * base)2
= ((base4)2 * base)2

= (((base2)2)2 * base)2

or, in Java,

BigInteger result = BigInteger.ONE;
result = result.multiply(base);
result = result.multiply(result);
result = result.multiply(result);
result = result.multiply(result);
result = result.multiply(base);
result = result.multiply(result);

We get the same result, but we perform only six multiplications instead of eighteen.

Drawing up the plan

Of course, this means that we have to figure out, for each separate exponent, an efficient plan for interleaving squaring operations and multiplications by base to get exactly the exponent we want. And in some cases it may not be obvious which of the possible plans is the most efficient.

If we have a lot of exponentiation problems to do, involving many different exponents, one approach to finding efficient is to set up a directed graph in which the vertices are possible exponents and each arc connects an exponent either to the next greater exponent (representing a multiplication by the base) or to an exponent twice as large (representing a squaring operation). So, for instance, the exponent 93 would have two arcs leading away from it -- one to the exponent 94, and one to the exponent 186. Of course, to keep the graph finite, we would have to impose a maximum on the exponents that can be represented inside the graph, and leave out any arcs that would take us to unrepresented exponents. (For instance, if we set a maximum of 1000, we wouldn't have an arc from 647 to 1294, because there would be no vertex for 1294.)

Finding the most efficient plan would then simply entail finding the shortest path from vertex 0 to the desired exponent in this graph. We could use Dijkstra's algorithm for this purpose.

Adjusting the arc weights

Normally, we assume that multiplication is a constant-time operation, which would correspond in our graph representation to the assumption that all the arc weights should be the same. But this isn't true in the BigInteger type. It's much more likely that the running times of the multiplications that we are contemplating depend on the sizes of the numbers that we are trying to multiply together.

A more realistic way to assess the running time for a multiplication by the base would be to label each arc from an exponent e to the next greater exponent e + 1 with the value e itself. And a more realistic way to assess the running time for a squaring operation would be to label each arc from an exponent e to its double 2e with e * lg*e. (A kind of divide-and-conquer algorithm can be used for multiplying large integers.)

The assignment

Write a Java program that builds an exponent graph, with appropriate arc weights, for exponents ranging from 0 to 1000, and then finds and prints the shortest path from vertex 0 to every other vertex in the graph.

This assignment will be due on Friday, May 9.