Calculating Large Powers

The Problem

suppose you want to calculate \(a^n\)

where \(a\) is an integer

and \(n\) is a very large positive integer

e.g. \(a^{65537}\) is often calculated in RSA cryptosystems

Some Special Cases

\(a^0\) is 1 if \(a \neq 0\)

\(0^n\) is 0 if \(n \neq 0\)

\(1^n\) is 1 if \(n \neq 0\)

\(a^1\) is \(a\) if \(a \neq 0\)

\(0^0 = 1\) (although sometimes it is said to be undefined)

The Obvious Approach (Iterative)

// Pre-condition:
//    n >= 0
// Post-condition:
//    returns a to the power of n, i.e. a^n
// Note:
//    The bases cases can be compressed somewhat. However, we list them
//    individually to make it easier to see that all the cases have been
//    handled.
int power_iter(int a, int n) {
    assert(n >= 0); // check pre-condition

    if (a == 0 && n == 0) return 1;
    if (a == 0 && n != 0) return 0;
    if (a != 0 && n == 0) return 1;
    if (a == 1) return 1;
    int pow = 1;
    for(int i = 0; i < n; ++i) {
        pow *= a;
    }
    return pow;
}

The Obvious Approach (Recursive)

int power_recur(int a, int n) {
    assert(n >= 0); // check pre-condition

    if (a == 0 && n == 0) return 1;
    if (a == 0 && n != 0) return 0;
    if (a != 0 && n == 0) return 1;
    if (a == 1) return 1;
    return a * power_recur(a, n - 1);
}

Performance of the Obvious Approach

for both the obvious iterative approach and the obvious recursive approach, the performance is as follows

\(a^2 = a \cdot a\) does 1 multiplication

\(a^3 = a \cdot a \cdot a\) does 2 multiplications

\(a^4 = a \cdot a \cdot a \cdot a\) does 3 multiplications

\(a^n = a \cdot a \cdot \ldots a \cdot a\) does \(n-1\) multiplications (for \(n > 1\))

in general, this obvious approach does \(n - 1\) multiplications to calculate \(a^n\)

notice that we count multiplications, since that is the key operation here

the number of multiplications is proportional to the running time of the functions

A Faster Algorithm

note the following pattern

\(a \cdot a = a^2\)

\(a^2 \cdot a^2 = a^4\)

only 2 multiplications were needed to get \(a^4\)

that’s one less than the obvious algorithm

\(a^4 \cdot a^4 = a^8\), needs only 1 more multiplication for a total of 3
(instead of 7 for the obvious algorithm)
\(a^8 \cdot a^8 = a^{16}\), needs only 1 more multiplication for a total of 4
(instead of 15 for the obvious algorithm)

A Faster Algorithm

this shows that “repeated squaring” can be used to calculate \(a^n\) more efficiently than the obvious algorithm

but this only works when \(n\) is a power of 2

can we find a way to make this trick work with any positive \(n\)?

A Faster Algorithm

to calculate \(a^{2k}\), we just square \(a^k\) (and recursively calculate \(a^k\))

but for \(a^{2k+1}\), squaring won’t work (because it’s not a perfect square)

however, we could re-arrange it like this: \(a^{2k+1} = a \cdot a^{2k}\)

then we square \(a^k\) and multiply that by \(a\)

A Faster Algorithm

int power_recur_fast(int a, int n) {
    assert(n >= 0); // check pre-condition

    if (a == 0 && n == 0) return 1;
    if (a == 0 && n != 0) return 0;
    if (a != 0 && n == 0) return 1;
    if (a == 1) return 1;

    int half = power_recur_fast(a, n / 2);
    if (n % 2 == 0) {
        return half * half;
    } else {
        return a * half * half;
    }
}

Counting Multiplications

lets count how many multiplications power_recur_fast does by adding a global variable called mult_count

int mult_count = 0;

int power_recur_fast(int a, int n) {
    assert(n >= 0); // check pre-condition

    if (a == 0 && n == 0) return 1;
    if (a == 0 && n != 0) return 0;
    if (a != 0 && n == 0) return 1;
    if (a == 1) return 1;

    if (n % 2 == 0) {
        int half = power_recur_fast(a, n / 2);
        mult_count += 1;
        return half * half;
    } else {
        int half = power_recur_fast(a, (n - 1) / 2);
        mult_count += 2;
        return a * half * half;
    }
}

void power_test(int a, int n) {
    mult_count = 0;
    power_recur_fast(a, n);
    cout << a << "^" << n << ", " << mult_count << " multiplications" << endl;
}

Counting Multiplications

now run some test code

for(int i = 0; i <= 20; ++i) {
    power_test(2, i);
}

output

2^0, 0 multiplications
2^1, 2 multiplications
2^2, 3 multiplications
2^3, 4 multiplications
2^4, 4 multiplications
2^5, 5 multiplications
2^6, 5 multiplications
2^7, 6 multiplications
2^8, 5 multiplications
2^9, 6 multiplications
2^10, 6 multiplications
2^11, 7 multiplications
2^12, 6 multiplications
2^13, 7 multiplications
2^14, 7 multiplications
2^15, 8 multiplications
2^16, 6 multiplications
2^17, 7 multiplications
2^18, 7 multiplications
2^19, 8 multiplications
2^20, 7 multiplications

the number of multiplications being done is less then for the obvious algorithm (obviously!)

but is there pattern to the number of multiplications?

Counting Multiplications

it turns out to be useful to print a message when each part of the if-statement is called

int power_recur_fast(int a, int n) {
    assert(n >= 0); // check pre-condition

    if (a == 0 && n == 0) return 1;
    if (a == 0 && n != 0) return 0;
    if (a != 0 && n == 0) return 1;
    if (a == 1) return 1;

    if (n % 2 == 0) {
        int half = power_recur_fast(a, n / 2);
        mult_count += 1;
        cout << 1;           // print 1
        return half * half;
    } else {
        int half = power_recur_fast(a, (n - 1) / 2);
        mult_count += 2;
        cout << 2;          // print 2
        return a * half * half;
    }
}

void power_test(int a, int n) {
    mult_count = 0;
    power_recur_fast(a, n);
    cout << a << "^" << n << ", " << mult_count << " multiplications"
         << endl;
}

output

2
21
22
211
212
221
222
2111
2112
2121
2122
2211
2212
2221
2222
21111
21112
21121
21122
21211

can you see a pattern in these values?

Counting Multiplications

it is a little easier to see if you change 1s to 0s and 2s to 1s

1
10
11
100
101
110
111
1000
1001
1010
1011
1100
1101
1110
1111
10000
10001
10010
10011
10100

these are the binary numbers from 1 to 20 (!)

a 0 means 1 multiplication is done

a 1 means two multiplications are done

a number \(n\) has about \(\log_2 n\) bits

at worst, if every bit is a 1, \(2\log_2 n\) multiplications would be done

this is much smaller than the \(n - 1\) multiplications done by the obvious algorithm

Performance

so we can say that, in the worst case, the number of comparisons done by the fast recursive power algorithm is proportional to \(\log_2 n\)

multiplication is a key instruction, so that implies that the overall running time is also likely proportional to \(\log_2 n\)

this makes it fast enough in practice to calculate large exponents (such as the RSA cryptosystems example at the beginning)

Note

this is an interesting algorithm!

the performance is not at all obvious at first, nor is the connection to binary numbers

you may wonder if this is the fastest way to calculate exponents

the answer is no (!)

for some exponents even fewer multiplications are possible

for example, consider \(2^{15}\)

get \(2^{3}\) in 2 multiplications

then square it to get \(2^{6}\), 1 more multiplication

then square it to get \(2^{12}\), 1 more multiplication

then multiply by \(2^{3}\) to get \(2^{15}\), 1 more multiplication

thus it takes 5 multiplications in total to calculate \(2^{15}\)

but the fast recursive algorithm takes 8 multiplications