• Java 2 Platform Standard Edition 5.0 API specification
• OpenJDK source code repository for library classes
• Source code from Data structures and problem-solving using Java, third edition
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)2base8 * base)2base4)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.
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.
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.)
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.