Posted in:

Day 20’s puzzle at Advent of Code was in one sense very simple, but if you weren’t careful, you’d create a solution that took ages. In today’s video, I explain how I initially came up with a very slow solution, and then showed some ways that dramatically speeded it up.

Here’s my first C# version, that uses a naive algorithm to calculate the presents for each house. Even trying to optimize by excluding houses without multiple prime factors from the search space didn’t really make up for how slow the present counting was.

Func<int,int> presentsForHouse = house => Enumerable.Range(1,house)
                                    .Where(elf => house % elf == 0)
                                    .Sum() * 10;
Func<int, int> presentsForHouseB = house => Enumerable.Range(1, house)
                                     .Where(elf => house % elf == 0 && house / elf <= 50)
                                     .Sum() * 11;

var fact = (2*3*5*7*11);
Enumerable.Range(1, 10000000)
    .Where(n => n % fact == 0)
    .Select(h => new { House = h, Presents = presentsForHouse(h) })
    .First(h => h.Presents >= 36000000).Dump("a");

var factB = (2 * 2 * 2 * 3 * 3);
Enumerable.Range(700000, 10000000)
    .Where(n => n % factB == 0)
    .Select(h => new { House = h, Presents = presentsForHouseB(h) })
    .First(h => h.Presents >= 36000000).Dump("b");

So in my F# version, I used a more intelligent approach, getting all the factors of the house number in order to work out which elf visited. The factors function is based on one by Jeff on the Advent of Code subreddit. I left in my optimization of only testing likely houses from C#. Overall this factors optimization sped up the calculation of part b’s answer from 18 minutes down to under a second!

let factors number = seq {
    for divisor in 1.. (float >> sqrt >> int) number do
        let a,b = number%divisor, number/divisor
        if a = 0 then
            yield divisor
            if not (divisor = b) then
                yield b }
                
let presentsForHouseA house = 
    factors house
    |> Seq.sum 
    |> ((*) 10)

let presentsForHouseB house = 
    factors house
    |> Seq.filter (fun factor -> house/factor <= 50)
    |> Seq.sum 
    |> ((*) 11)

let search target func testSeq =
    testSeq
    |> Seq.map (fun house -> (house, (func house)))
    |> Seq.find (fun (h,p) -> p > target) |> fst

let target = 36000000

let testNums rstart factor = 
    seq { for n in rstart..target do if n % factor = 0 then yield n }

testNums 700000 (2*3*5*7*11)
|> search target presentsForHouseA 
|> printfn "a: %d" //831600

testNums 700000 (2*2*2*3*3)
|> search target presentsForHouseB 
|> printfn "b: %d" // 884520

But what’s interesting is that there is a much simpler way to solve this problem which also happens to perform very fast. Annoyingly it’s the first solution I turned to, but then abandoned quickly. Basically, have an array of presents for each house, and then for each elf, update the total for all the houses they visit. This works almost as fast as the optimized F# solution, and could be optimised further if necessary by stopping as soon as any house has more than the target number of presents, and reducing the number of houses calculated as it is overkill to try so many. But here’s this solution in C#, and well done to r_sreeram who used it to get first place on the leaderboard.

var target = 36000000;
var houses = new int[target/10 + 1];
for (int elf = 1; elf < houses.Length; elf++)
    for (int house = elf; house < houses.Length; house+=elf)
        houses[house] += elf * 10;
for (int house = 1; house < houses.Length; house++)
    if (houses[house] > target) { house.Dump("a"); break; }

houses = new int[target/11 + 1];
for (int elf = 1; elf < houses.Length; elf++)
    for (int house = elf, n = 0; house < houses.Length && n < 50; house+=elf, n++)
        houses[house] += elf * 11;
for (int house = 1; house < houses.Length; house++)
    if (houses[house] > target) { house.Dump("b"); break; }
Want to learn more about LINQ? Be sure to check out my Pluralsight course LINQ Best Practices.