Special Pythagorean Triplet
Project Euler presents the following Problem #9:
A Pythagorean triplet is a set of three natural numbers, , for which,
For example, .
There exists exactly one Pythagorean triplet for which . Find the product .
Definitions
Pythagorean Triplet: Refers to a set of three positive integers , such that a right-angled triangle exists with the largest value as the hypotenuse, thereby satisfying the equation above. The first five triplets are: (3, 4, 5), (6, 8, 10), (5, 12, 13), (9, 12, 15), (8, 15, 17). The term "triplet" is used interchangeably with "triple" depending on the source.
Primitive Pythagorean Triplet: Refers to a Pythagorean triplet, such that . The first five primitive triplets are: (3, 4, 5), (5, 12, 13), (8, 15, 17), (7, 24, 25), (20, 21, 29). All non-primitive triplets can be generated from a primitive one.
Coprime: Two integers are considered coprime if their only common positive divisor is 1. This property is also referred to as "relatively prime" or "mutually prime".
Pythagorean triplets have special properties (many that we won't cover):
- If primitive, one of or must be even and the other odd, with always being odd. This means that all generated triplets will switch between having all their elements be even (from even factors) or all their elements following the same pattern as the original primitive (from odd factors). For example, (3, 4, 5) (6, 8, 10) (9, 12, 15) (12, 16, 20)
- Property #1 then dictates that the perimeter of a Pythagorean triplet triangle must always be even, as the sum of all even numbers is also an even number, as is the sum of two odd numbers.
- At least one element of every triplet is divisible by 3, 4, or 5, although an element may not be divisible by any of those factors.
- The product of the two non-hypotenuse sides of a Pythagorean triplet triangle is always divisible by 12 and the product is always divisible by 60.
Let's use a few of these properties to implement three functions that take a perimeter and return the largest product if a triplet is found whose elements sum to ; otherwise, the functions will return -1. Note that, while the original problem makes it clear that only one triplet exists for , triplets that share the same do exist.
Nested Loop Solution
This first solution iterates through combinations of with an outer loop representing values of and an inner loop representing , while using some of the above properties to limit the amount of considered triplets.
First, any value that is not even (property #2) is immediately rejected as it will not have a valid triplet.
Second, is constrained to being greater than , but not greater than . Its lower limit is based on the following:
The upper limit of can be shown based on the following:
Third, must of course be smaller than , but should not drop below .
Fourth, must be greater than , so iterating backwards through these two loops allows us to break out of the inner loop early and start fresh with a new if ends up being too small in value.
Once a triplet has reached this point, it only needs to satisfy two scenarios: property #4 and the Pythagoras Theorem. hypot() from the kotlin.math package solves the formula to validate the triplet as being Pythagorean. Once a valid triplet is found, there is no point in iterating over further values of as any future combinations would result in a product of lesser value.
1import kotlin.math.hypot23fun pythagoreanTripletBC(p: Int): Int {4 if (p % 2 != 0) return -156 var product = 07 nextC@for (c in (p / 2 - 1) downTo (p / 3 + 1)) {8 val diff = p - c9 nextB@for (b in (c - 1) downTo (diff / 2)) {10 val a = diff - b11 if (b <= a) continue@nextC12 if (a * b % 12 != 0) continue@nextB13 if (hypot(a.toDouble(), b.toDouble()) == c.toDouble()) {14 product = maxOf(product, a * b * c)15 break16 }17 }18 }1920 return product21}
📊 pythagoreanTripletBC() checked 51,758 triplets by invoking hypot() when p = 3000. Let's see how a single loop solution can reduce this amount in the next section.
Single Loop Solution
The nested loop solution can be altered to use only one for loop by focusing on instead of .
As before, only even values of are considered. Then, given that (3,4,5) is the smallest existing triplet, must be greater than or equal to 3 and cannot exceed (based on a similar breakdown that reached in the previous section).
If is inserted into the Pythagoras Theorem, the formula reduces to:
This formula can be used to calculate for every , then the same conditions as for the first solution can be applied to find a valid triplet. Exhaustive search shows that iterating backwards through potential values of produces the correct output as the first valid triplet, so the loop can be broken early.
1import kotlin.math.hypot23fun pythagoreanTripletA(p: Int): Int {4 if (p % 2 != 0) return -156 var product = 07 for (a in (p / 3 - 1) downTo 3) {8 val b = p * (p - 2 * a) / (2 * (p - a))9 if (a >= b || a * b % 12 != 0) continue10 val c = p - a - b11 if (hypot(a.toDouble(), b.toDouble()) == c.toDouble()) {12 product = a * b * c13 break14 }15 }1617 return product18}
📊 pythagoreanTripletA() gives a significant performance boost by only checking 38 triplets using hypot() when p = 3000.
Alternative Solution
The single loop solution is more than performant enough for our problem goal, but it would be prudent to cover a less straightforward (but still efficient) approach, particularly since it will be a useful algorithm for future problems (as early as Problem 39).
This solution will make use of the so far unmentioned term "Primitive Pythagorean Triplet" that was defined at the beginning of this post.
Based on the fact that all Pythagorean triplets can be derived from a primitive triplet, we can reduce all triplets to their primitive form by dividing out the factor. Then each primitive triplet can be generated from a unique pair of positive coprime integers and based on Euclid's formula:
We'll first implement a helper function based on these conditions and equations:
1import kotlin.math.abs23/**4 * Euclid's formula generates all Pythagorean triplets from 2 numbers, m and n.5 *6 * All triplets originate from a primitive one by multiplying them by d = gcd(a,b,c).7 *8 * @throws IllegalArgumentException if arguments do not follow m > n > 0, or if not exactly9 * one is even, or if they are not co-prime, i.e. gcd(m, n) != 1.10 */11fun pythagoreanTriplet(m: Int, n: Int, d: Int): Triple<Int, Int, Int> {12 require(n in 1 until m) { "Positive integers assumed to be m > n > 0" }13 require((m % 2 == 0) xor (n % 2 == 0)) { "Integers must be opposite parity" }14 require(isCoPrime(m, n)) { "Positive integers must be co-prime" }1516 val a = (m * m - n * n) * d17 val b = 2 * m * n * d18 val c = (m * m + n * n) * d19 return Triple(minOf(a, b), maxOf(a, b), c)20}2122fun isCoPrime(x: Int, y: Int): Boolean = gcd(x.toLong(), y.toLong()) == 1L2324/**25 * Recursive calculation of the greatest common divisor of n1 and n2 using the26 * Euclidean algorithm.27 *28 * gcd(x, y) = gcd(|y % x|, |x|); where |y| >= |x|29 *30 * gcd(x, 0) = gcd(0, x) = |x|31 */32fun gcd(n1: Long, n2: Long): Long {33 val x = abs(n1)34 val y = abs(n2)35 if (x == 0L || y == 0L) return x + y36 val bigger = maxOf(x, y)37 val smaller = minOf(x, y)38 return gcd(bigger % smaller, smaller)39}
Now that we know how to get a triplet from , we need to determine how to choose appropriate values for these arguments.
If we insert the three equations from Euclid's formula into the perimeter formula, the latter becomes:
So, at its minimum, must be greater than 1 (thereby allowing ) and cannot exceed . If is rearranged to solve for , it becomes:
So, for to be a positive integer, we must disregard any value of that is not a factor of . This is what our solution looks like so far:
1import kotlin.math.ceil2import kotlin.math.sqrt34fun pythagoreanTripletAlt(p: Int): Int {5 if (p % 2 != 0) return -167 var product = 08 val limit = p / 29 val mMax = ceil(sqrt(1.0 * limit)).toInt()10 for (m in 2 until mMax) {11 if (limit % m == 0) {12 // TODO()13 // Find suitable n values14 // Iterate through d values & generate triplets15 // Keep track of max product for each triplet found16 }17 }1819 return product20}
For every valid , we have to find valid values of . If we consider , we can extract a new value as a starting point, which is adjusted based on the parity of to ensure an opposite parity for . will be incremented by 2 (thereby incrementing while ensuring its parity is retained) without exceeding either or . The latter can be shown by , whereas the former is based on the following:
Here is our updated solution:
1import kotlin.math.ceil2import kotlin.math.sqrt34fun pythagoreanTripletAlt(p: Int): Int {5 if (p % 2 != 0) return -167 var product = 08 val limit = p / 29 val mMax = ceil(sqrt(1.0 * limit)).toInt()10 for (m in 2 until mMax) {11 if (limit % m == 0) {12 var k = if (m % 2 == 1) m + 2 else m + 113 var kMax = limit / m14 while (kMax % 2 == 0) {15 kMax /= 216 }17 while (k < 2 * m && k <= kMax) {18 // TODO()19 k += 220 }21 }22 }2324 return product25}
If the non-primitive versions of the equations are used in the perimeter formula, we can see how a value of is found:
So, to ensure a positive integer , needs to be a factor of . Also, for and to be coprime, and must also be coprime.
Finally, once a valid is found, pythagoreanTriplet() is called with , , and , as shown in the final solution:
1import kotlin.math.ceil2import kotlin.math.sqrt34fun pythagoreanTripletAlt(p: Int): Int {5 if (p % 2 != 0) return -167 var product = 08 val limit = p / 29 val mMax = ceil(sqrt(1.0 * limit)).toInt()10 for (m in 2 until mMax) {11 if (limit % m == 0) {12 var k = if (m % 2 == 1) m + 2 else m + 113 var kMax = limit / m14 while (kMax % 2 == 0) {15 kMax /= 216 }17 while (k < 2 * m && k <= kMax) {18 if (kMax % k == 0 && isCoPrime(k, m)) {19 val (a, b, c) = pythagoreanTriplet(m, k - m, limit / (m * k))20 product = maxOf(product, a * b * c)21 }22 k += 223 }24 }25 }2627 return product28}
📊 pythagoreanTripletAlt() generated only 3 Pythagorean triplets when p = 3000. A less direct approach, but well worth having in your repertoire!
Here's a breakdown of the average execution times on my system over 100 iterations at different values of :
| p | Nested Loop | Single Loop (ns) | Alternative (ns) |
|---|---|---|---|
| 1000 | 9.9e+05ns | 6.1e+04 | 2.2e+04 |
| 3000 | 2.03ms | 1.1e+05 | 5.1e+04 |
📚 Want to read more about Pythagorean triplets? Here are some references about other ways to generate triplets, as well as more in-depth proofs of the theorems used in this post.
Here's the full source code for this problem (and its corresponding test suite).
This solution set is also available in Python and in C++ on my GitHub.
Problem content on the Project Euler site is licensed under a Creative Commons (CC) License: CC BY-NC-SA 4.0. Solutions to only the first hundred problems are encouraged to be distributed publicly.
Thanks for reading!🏝️📐📐📐