This is the twelfth part of the ILP series. For your convenience you can find other parts in the table of contents in Part 1 – Boolean algebra
In Part 3 we saw how to calculate exponentiation with constant exponent. Today we are going to implement the same operation, however, this time exponent will be a variable. Let’s begin.
Table of Contents
Introduction
First, let’s reiterate some assumptions we made during this course. We assumed that every variable has value not greater than . We introduced this constraint in Part 2 – Multiplication in order to be able to decompose the value using binary representation. However, we didn’t assume that the result of multiplication still hold this constraint — it could be much greater than . Of course we would not be able to utilize this option if we would try to multiply the result again.
We could do the same with exponentiation, e.g., assume that operands are not greater than , but the result is. This is a great idea, however, we are probably not able to perform calculations on such a big values because of floating point representation. Imagine that we want to calculate — the result is so big that we will not be able to perform further calculations.
That is why we will make an assumption that the result is not greater than . This will simplify the problem and also allow us to implement really fast algorithm.
Test cases
Let’s assume that which implies . Using variables we would like our operation to fulfill the following requirements:
In other words: every variable raised to the power of zero should be equal to one. Notice that we define as one because we cannot throw exception or ignore these values, what’s more, everyone does that. We want every value raised to the power of one be equal to the value itself, even very big value (as big as ). We also want to be able to calculate every exponentiation for which result is not greater than (so we want to be able to calculate and ).
Corner cases
Let’s start with corner cases. Assuming that is our result, we have:
We have the following cases:
- is zero then we return one — because every value (even ) raised to the power of zero is equal to one (tests 1-5)
- is one then we return — because very value raised to the power of one is equal to the value itself (tests 6-10)
- is not greater than one then we return — because one raised to any power is still equal to one (tests 11-12)
We also introduced which represents the ”casual” case — the case which needs actual exponentiation (tests 13-15).
Exponentiation by squaring
We will not avoid multiplication. We know how to do fast multiplication but we still want to multiply as little as possible. We can notice that when calculating we don’t need to multiply nine times (e.g., ) because . Exponentiation by squaring is based on this idea and we will do the same.
We assumed that . This means that so in binary representation it has at most digits. Because we assumed that the result is not greater than we can safely ignore six digits — why? There are only two values which result of raising to the power of ten (and greater) can be represented using ten digits: these are zero and one. Indeed, . So there is no need in handling exponents greater than nine because we cannot represent the result anyway. And we can represent using exactly four digits (which is ) and that is why we can ignore the rest of them.
This means that we need to represent using binary digits.
First formula
We are ready to define . This time we will use pseudo-code to define the algorithm:
1 2 3 4 5 6 |
currentPower = a result = 1 digits = representation of a (from right to left) for i = 0..digits.length do if i > 0 then currentPower = currentPower * currentPower result = result * max(1, currentPower * digits[i]) |
With every loop iteration we calculate square of current power. Next, we multiply the result by the current power multiplied by the next digit or one. Let’s consider an example . We have and . We have the following values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Before entering the loop: currentPower = 2 result = 1 digits = 0101 Iteration one: i = 0, digits[0] = 1 i is not greater than 0 so we have currentPower = 2 result = result * max(1, 2 * 1) = result * 2 = 2 Iteration two i = 1, digits[1] = 0 i is greater than 0 so we have currentPower = currentPower * currentPower = 4 result = result * max(1, 4 * 0) = result = 2 Iteration three: i = 2, digits[2] = 1 i is greater than 0 so we have currentPower = currentPower * currentPower = 16 result = result * max(1, 16 * 1) = 32 Iteration four: i = 3, digits[3] = 1 i is greater than 0 so we have currentPower = currentPower * currentPower = 256 result = result * max(1, 256 * 0) = 32 |
Looks great! But…
There is a problem with test 5. Observe that we calculate the square of the current power in every loop iteration. As we stated before it is not a problem because we don’t require the result of multiplication to be not greater than , so we can easily calculate . But when we perform another loop iteration we need to represent the result of previous multiplication using exactly ten binary digits which is impossible!
But we handle corner cases!
You might think that there is no problem because we have already handled the corner cases. But this is not true. We need to remember that ILP is a declarative paradigm and we need to represent every possible operation outcome. Yes, we use conditions to select correct value for edge case, but every operand is evaluated and we just cannot calculate the value for false branch.
Second formula
So how do we get rid of this problem? We can easily check whether we have to calculate edge case and make current power smaller:
1 2 3 4 5 6 |
currentPower = isThisCornerCase ? 1 : a result = 1 digits = representation of a (from right to left) for i = 0..digits.length do if i > 0 then currentPower = currentPower * currentPower result = result * max(1, currentPower * digits[i]) |
We have modified only one line (the first one) but now everything works great. For edge case we raise one instead of and everything should be great. However, there is a problem with test 15: . In every loop iteration we still calculate the square of the current power even if there are no more non-zero digits left. Let’s see this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Before entering the loop: currentPower = 10 result = 1 digits = 0011 Iteration one: i = 0, digits[0] = 1 i is not greater than 0 so we have currentPower = 10 result = result * max(1, 10 * 1) = result * 10 = 10 Iteration two i = 1, digits[1] = 1 i is greater than 0 so we have currentPower = currentPower * currentPower = 100 result = result * max(1, 100 * 1) = result = 1000 Iteration three: i = 2, digits[2] = 0 i is greater than 0 so we have currentPower = currentPower * currentPower = 10000 result = result * max(1, 10000 * 0) = 1000 Iteration four: i = 3, digits[3] = 0 i is greater than 0 so we have currentPower = currentPower * currentPower = 10000 * 10000 = error! we cannot represent 10000 using 10 digits and we cannot perform multiplication |
So how do solve this problem? In every loop iteration we can check whether it makes sense to calculate next square. If there are no more non-zero digits than we can stop squaring the current power (but we need to continue loop – we are declaring results, not calculating them on the fly!). So we get:
1 2 3 4 5 6 7 8 |
currentPower = isThisCornerCase ? 1 : a result = 1 digits = representation of a (from right to left) for i = 0..digits.length do if i > 0 then currentPower = min(currentPower, thereAreNonZeroDigitsLeft * k) currentPower = currentPower * currentPower result = result * max(1, currentPower * digits[i]) |
As you can see, when there are no more non-zero digits then currentPower is set to zero (because second argument of function is equal to zero).
Summary
We are now able to calculate exponentiations with non-constant exponent. We are also able to calculate roots with non-constant degree using the same reasoning as for constant degree in Part 3. In the next part we will calculate factorial function. See you soon!