Pythagorean Triple Problem in Sub-linear Time
The Pythagorean triple problem is as follows. Given an input integer \(n\), return integers \(a\), \(b\), \(c\) such that the two following conditions hold:
$$ a b c = n $$ $$ a^2 + b^2 = c^2 $$
I was interested in finding a solution to this problem that was both succint and
had good asymptotic complexity. The solution I found runs in O(sqrt(n))
time
by deconstructing the problem into the well-known
3SUM problem.
Getting the Divisors
We know that the three numbers we generate must all multiply together to form
\(n\). Therefore, each number must be a divisor of \(n\). There is a
simple O(sqrt(n))
time algorithm that generates all divisors of \(n\):
// @require n >= 1
export const divisors = (n: number) => {
const d = _.times(Math.sqrt(n) - 1)
.map(i => i + 1)
.filter(i => n % i === 0);
return _.uniq(d.concat([...d].reverse().map(i => n / i)));
};
The algorithm is expressed in TypeScript, in a functional form. The algorithm
takes all numbers in the range of [1 ... sqrt(n)]
and filters such numbers
that \(n\) is divisble by. We are left with all of the divisors up until
\(\sqrt n\).
To then get the rest of the numbers, concatenate the current array with each divisor’s pair. This is because if \(i\) is a divisor, \(\frac{n}{i}\) is also guaranteed to be a divisor. All references to _ are lodash.
Invoking the 3SUM Problem
We now have a list of numbers to search from to achieve the two conditions. The
length of the list is on order of O(log(n))
because that is, up to a constant
factor, how many divisors a given number has.
On inspection, we expect the second condition to be more “stringent” i.e. there exists fewer combinations which satisfy the condition. Luckily, there exists a body of knowledge on solving that sort of problem.
The 3SUM Problem
The 3SUM problem is to, given a list of numbers \(A\), return a set of three numbers \(a\), \(b\), \(c\) such that the following conditions hold:
$$ a + b + c = 0 $$ $$ a, b, c \in A $$
There are many algorithms to solve this, including a relatively simple \(O(n^2)\) solution. However, this does not quite match our problem. However, if we squint our eyes a bit, we can see how it may be applied. We may perform some simple algebra on our condition:
$$ a^2 + b^2 = c^2 $$ $$ a^2 + b^2 - c^2 = 0 $$
So we see if we include all negative numbers of our divisor into our search set \(A\), we’re much better off. As well, we square each number of our original divisor set. So, given a divisor set for example, of 30:
$$ {1, 2, 3, 5, 6, 10, 15, 30} $$
We transform this set into the following:
$$ {-900, -225, -100, -36, -25, -9, -4, -1, 1, 4, 9, 25, 36, 100, 225, 900} $$
The 3SUM search is guaranteed to find a 3-set matching our original Pythagorean condition. However, it will also match false-positives constructed of more than one negative number. To filter these out, we only consider solutions to the 3SUM problem which possess one negative number.
Putting it all Together
The following code implements the algorithm described above, taking the divisor set, transforming it, applying it to the 3SUM problem, and filtering the results. The overall complexity is \(O(\sqrt{n})\) because the complexity of constructing the divisors is strictly more expensive than solving the 3SUM problem on the divisor set. The complexity could probably be improved via Pollard’s Rho algorithm, at the cost of sacrificing simplicity.
// Returns [a, b, c] where a^2 + b^2 = c^2 and a * b * c = n
// If no such 3-tuple exists, returns [].
// Runs in O(sqrt(n)) time.
// @require n >= 1
export const pythagoreanTriplet = (n: number) => {
let d = divisors(n).map(x => x ** 2);
d = [...d]
.reverse()
.map(x => -x)
.concat(d);
// O(log(n)^2)
const p = sum3(d)
.filter(x => _.countBy(x, y => y < 0).true === 1)
.map(x => x.map(y => Math.sqrt(Math.abs(y))).sort((a, b) => a - b))
.filter(x => x.reduce((a, y) => a * y) === n);
return p.length > 0 ? p[0] : [];
};