Notes for competitive programming
Number theory 3: Extended Euclidean algorithm
Key ideas
- The extended Euclidean algorithm extends the basic Euclidean algorithm calculates the smallest possible that satisfies the equation .
- At the base case, we let ; for each recursive step, we let , where (or
a / b). - To implement the algorithm iteratively instead of recursively, we calculate a recursive sequence for and , given as with .
In 14.02, we briefly discussed Bezout’s identity. This states that for integers and such that , then we can find integers and such that
In many cases, it would be useful to find the values of and that satisfy this equation. For example, , and we want to find the values of and such that . A solution would be . This solution is not unique; another possible solution would be .
Euclid’s algorithm
It turns out that Euclid’s algorithm, with additional bookkeeping, can help us find and . To recall, Euclid’s algorithm uses the following recurrence:
So for the pair , Euclid’s algorithm does the following transition:
Let’s try to visualize what Euclid’s algorithm is actually doing and why it works by analyzing the state transitions. Notice that two successive states share an element:
The element added by state transition arrows represent the operation . Now recall the definition of the modulo:
for some positive . For example,
Here, is 3, since . We’ll take note of this for later.
We can prove that this transition “preserves” the GCD, i.e. . To prove this, we can use the division theorem, which states that for some quotient .
By divisibility rules, if an integer divides and , then it must divide , since is a linear combination of and . We also note that , so if an integer divides and , then it must divide , since is a linear combination of and .
Restating the paragraph above, if divides and , it also divides , and if divides and , it also divides . Bridging these two facts together, if divides and , it must also divide and , and vice versa. Thus, the pair shares the same divisors as the pair , and they must share the same GCD.
Now this process must terminate, since the second element keeps on getting smaller (). Since the modulo operation never becomes negative for two positive inputs, at some point, must be . In fact, we can prove that the first element is halved every two iterations (), which proves that Euclid’s algorithm is .
Bookkeeping
Let’s extend Euclid’s algorithm by keeping track of . Let’s zoom in on the first transition state:
From before, we know that , with . If we move the on the right side, then we get
Note that this already gives us the exact formulas for Bezout’s identity: and . So we see that this formula for is somehow key to getting the correct values of and .
Let’s try to look at a bigger example to see how to get the correct values of and . The following is a trace table for . Note that, as we found before, .
| 305 | 65 | 45 | = 305 - 4 x 65 |
| 65 | 45 | 20 | = 65 - 1 x 45 |
| 45 | 20 | 5 | = 45 - 2 x 20 |
| 20 | 5 | 0 | = 20 - 4 x 5 |
| 5 | 0 | - | - |
To recover and , we can start from the following equation in the bottom row:
where . The row above states that , so we substitute that in:
So as went from back to , transitioned from . Let’s repeat this transition:
Let’s repeat it again:
And one last time:
So the transitions for are , such that the final coefficients for Bezout’s identity are . It can be proven that this is the pair of values that minimizes .
In general, when we transition from , noting that , we do the following transition:
So at each step, we transition from . Recall that .
Recursive algorithm
To calculate and along with the GCD , we thus need to implement this state transition. Let’s start off with the basic Euclidean algorithm. This is an expanded version of the one-liner algorithm from 14.02.
int gcd(int a, int b) { if (b == 0) { return a; } int k = gcd(b, a % b); return k;}We want to calculate and as well, so we’ll return a 3-tuple . As our base case, , since .
tuple<int, int, int> ext_gcd(int a, int b) { if (b == 0) { return {a, 1, 0}; } int k = gcd(b, a % b); return k;}ext_gcd now returns a tuple, so we’ll have to recover . Remember that .
tuple<int, int, int> ext_gcd(int a, int b) { if (b == 0) { return {a, 1, 0}; } auto [k, xp, yp] = ext_gcd(b, a % b); return k;}Once we’ve gotten the GCD, we need to work our way backwards, like in our trace table above. We need to calculate and for our current pair, based on the pair below it in the table. From before, we know that this is given by the state transition . Note that this is given by for the current pair and not the pair we just calculated in line 5.
To simplify our code, we note that our calculation for is equivalent to integer division, a / b.
tuple<int, int, int> ext_gcd(int a, int b) { if (b == 0) { return {a, 1, 0}; } auto [k, xp, yp] = ext_gcd(b, a % b); int x = yp, y = xp - yp * (a / b); return {k, x, y};}You can confirm that this function calculates and in the same way as our problem above, starting with and ending at the correct values for and .
Iterative algorithm
Converting this algorithm to work alternatively is a bit difficult because our state transition for works from base case upwards, in the reverse direction of how we compute .
If we look at how Euclid’s algorithm transitions between states:
Since each pair of values has a clear overlap, we can restate this as a sequence:
This sequence is a sequence of remainders, with , , and (for some quotient ). The sequence terminates when , with the GCD being .
We can see if we can express as a sequence . We want to maintain the following invariant for any :
This way, when , we have
which is what we want.
We first set and to make the following two equations correct:
Now . If we substitute in the equations for and as above, we get:
and thus , . This holds for greater values of , so in general, we have
which exactly parallels the recurrence for . We can easily implement this using the code below:
tuple<int, int, int> ext_gcd(int a, int b) { int xi = 1, yi = 0, ri = a; int xii = 0, yii = 1, rii = b; while (rii != 0) { int m = ri / rii; tie(xi, xii) = {xii, xi - m * xii}; tie(yi, yii) = {yii, yi - m * yii}; tie(ri, rii) = {rii, ri - m * rii}; } return {ri, xi, yi};}Note that tie(i, j) creates an “assignable pair”; C++ will get the pair on the right, unpack it, and assign it to each individual value on the left.
- Module created
- Last updated