exercism.io rust track: Pythagorean Triplet

May 30, 2021 – 100 days to offload countdown #97

One of my on-and-off technical relationships is with exercism.io. A look on my profile there reveals that I have an interest in interesting languages and not a lot of stamina w/r to keeping up my exercises.

Today I had a few brain-cycles to spare and took a look at the rust track: I stopped a couple of years ago checking out the Pythagorean Triplet exercise, never tackling it. The original exercise was Project Euler #9: Find a pythagorean triplet so that it's sum equals 1000 (i.e. a^2 + b^2 = c^2 and a + b + c = 1000) and return the product a*b*c. I read up on algorithms to compute pythagorean triplets and found dicksons way the easiest to implement – I didn't want to use the straight nested loop solution that was offering itself as easiest to implement.

The first step in implementing dicksons algorithm was a factorization function. I found an implementation in a crate that found all prime factors of a given number – I took it and modified it to find all pairs of factors.

I than calculated all triples for values of `r`, checking if the sum is 1000 and returning the product of the matching triple.

The test passed and I submited my solution only to find out that the exercise was updated. The code must now find and return all triples whose sum is an arbitray value – as a HashSet. That got me scrambling for a bit – adapting my solution would have been easy enough, but I used a Vector of u32 3-tuples as data structure and had to figure out how to use HashSet. That took a bit but I figured it out as far as I needed to solve the problem without upsetting the rust compiler.

The final obstacle was the last test: Finding all triplets with the sum 30000. The code would simply take too long – I never let it run through, stopped it after a couple of minutes. I tried to use memoization for factorize, but the test never ran in acceptable time. I realized that I can limit the values for the smaller factor of the pairs found and finish factorize early – this ran in acceptable time. I tried to run the code without memoization and found out it had no real impact – the solution would solve the problem without it.

This is not elegant code, and not correctly optimized w/r to the limits that cut the necessary computations short - but for about 2 hours of my time figuring out the algorithm and the syntax I'm quite content.

use std::collections::HashSet;
use std::iter::FromIterator;

fn factorize(n: u32, limit: u32) -> Vec<(u32, u32)> {
    let mut pfs: Vec<(u32, u32)> = Vec::new();
    let mut d = 2;
    let mut q;
    while d <= n {
        while n % d != 0 {
            d += 1;
        }
        q = n / d;
        pfs.push(if d < q  { (d, q) } else { (q,d) });
        d += 1;
        if d>=limit { d = n + 1 }
    }
    pfs.sort();
    pfs.dedup();
    pfs
}

fn dickson(r: u32, limit: u32) -> HashSet<[u32; 3]> {
    let u : u32 = (r.pow(2))/2;
    HashSet::from_iter(factorize(u, limit).into_iter().map(|pair| [r+pair.0,r+pair.1,r+pair.0+pair.1]))
}

fn test_sum(r: u32, sum: u32) -> HashSet<[u32; 3]> {
    dickson(r, sum/3).into_iter().filter(|x| x[0]+x[1]+x[2] == sum).collect()    
}

pub fn find(sum: u32) -> HashSet<[u32; 3]> {
    (2..(sum/2)+1).step_by(2).flat_map(|r| test_sum(r, sum)).collect()
}