Advent of Code Day 15–Cookie Calorie Counting
In day 15 of the Advent of Code challenge we’re trying to make the most delicious cookie possible, using 100 teaspoons of ingredients. In today’s video I explain how I solved this challenge in C# using LINQ as well as an F# version of the solution
My C# code isn’t particularly optimal. I went for an Ingredient
class and I did decide to overload the +
and *
operators as a way to make the cookie scoring simpler. However, as I said in the video, my initial solution to distributing the 100 teaspoons between the 4 ingredients was over-complicated. I made a solution (the Distribute
method) that worked for any number of ingredients, but had I just made one that worked for 4, the code could be greatly simplified. The Distribute4
method shows how this can be done.
void Main()
{
var realInput = new[] {
"Frosting: capacity 4, durability -2, flavor 0, texture 0, calories 5",
"Candy: capacity 0, durability 5, flavor -1, texture 0, calories 8",
"Butterscotch: capacity -1, durability 0, flavor 5, texture 0, calories 6",
"Sugar: capacity 0, durability 0, flavor -2, texture 2, calories 1"
};
var ingredients = realInput
.Select(i => i.Replace(",", "").Replace(":", "").Split(' '))
.Select(p =>
new Ingredient
{
Capacity = int.Parse(p[2]),
Durability = int.Parse(p[4]),
Flavor = int.Parse(p[6]),
Texture = int.Parse(p[8]),
Calories = int.Parse(p[10])
}
)
.ToArray();
var scores = Distribute4(100) // or Distribute(new int[ingredients.Length], 100, 0)
.Select(r => ScoreCookie(ingredients, r))
.ToArray();
scores.Max(r => r.Item1).Dump("a"); //18965440
scores.Where(r => r.Item2 == 500).Max(r => r.Item1).Dump("b"); //18965440
}
Tuple<int,int> ScoreCookie(Ingredient[] ingredients, int[] amounts)
{
var p = ingredients
.Zip(amounts, (ing, amount) => ing * amount)
.Aggregate((a, b) => a + b);
return Tuple.Create(p.Score, p.Calories);
}
class Ingredient
{
public int Capacity { get; set; }
public int Durability { get; set; }
public int Flavor { get; set; }
public int Texture { get; set; }
public int Calories { get; set; }
public static Ingredient operator +(Ingredient x, Ingredient y)
{
return new Ingredient {
Capacity = x.Capacity + y.Capacity,
Durability = x.Durability + y.Durability,
Flavor = x.Flavor + y.Flavor,
Texture = x.Texture + y.Texture,
Calories = x.Calories + y.Calories
};
}
public static Ingredient operator *(Ingredient x, int n)
{
return new Ingredient {
Capacity = x.Capacity * n,
Durability = x.Durability * n,
Flavor = x.Flavor * n,
Texture = x.Texture * n,
Calories = x.Calories * n
};
}
public int Score
{
get { return Math.Max(0, Capacity) * Math.Max(0, Texture) * Math.Max(0, Flavor) * Math.Max(0, Durability); }
}
}
IEnumerable<int[]> Distribute(int[] start, int target, int len)
{
var remaining = target - start.Sum();
if (len == start.Length - 1)
{
var x = start.ToArray();
x[len] = remaining;
yield return x;
}
else
{
for (int n = 0; n < remaining; n++)
{
var x = start.ToArray();
x[len] = n;
foreach (var d in Distribute(x, target, len + 1))
{
yield return d;
}
}
}
}
IEnumerable<int[]> Distribute4(int max)
{
for (int a = 0; a <= max; a++)
for (int b = 0; b <= max - a; b++)
for (int c = 0; c <= max - a - b; c++)
yield return new[] { a, b, c, max - a - b - c};
}
As for F#, I decided against an Ingredient type, and went just for arrays of integers. This meant I needed to work out how to multiply every value in an array by a single number and how to add together several integer arrays with the same number of elements. This is done with Seq.reduce
and Array.map2
. As with the C# solution I overthought distributing the teaspoons between the ingredients. The F# distribute
is a bit nicer than the C# one, but I also show a distribute4
which is what I probably should have used.
let input = [|"Frosting: capacity 4, durability -2, flavor 0, texture 0, calories 5";
"Candy: capacity 0, durability 5, flavor -1, texture 0, calories 8";
"Butterscotch: capacity -1, durability 0, flavor 5, texture 0, calories 6";
"Sugar: capacity 0, durability 0, flavor -2, texture 2, calories 1"|]
let ingredients = input |> Array.map (fun f -> [| for m in Regex.Matches(f,"\-?\d+") -> int m.Value |])
let rec distribute state total maxlen = seq {
let remaining = total - (Seq.sum state)
match List.length state with
| l when l = maxlen - 1 -> yield remaining::state
| _ -> for n in 0..remaining do yield! distribute (n::state) total maxlen
}
let scoreCookie amounts =
let p = ingredients
|> Seq.zip amounts
|> Seq.map (fun (amount, ing) -> ing |> Array.map ((*) amount))
|> Seq.reduce (Array.map2 (+))
let score = (max 0 p.[0]) * (max 0 p.[1]) * (max 0 p.[2]) * (max 0 p.[3])
(score, p.[4])
let scores =
distribute [] 100 ingredients.Length
|> Seq.map scoreCookie
|> Seq.toArray
scores
|> Seq.map fst
|> Seq.max
|> printfn "a: %d" //18965440
scores
|> Seq.maxBy (fun (s,c) -> match c with | 500 -> s | _ -> 0)
|> fst
|> printfn "b: %d" // 15862900
// improvements:
let distribute4 m =
seq { for a in 0 .. m do
for b in 0 .. (m-a) do
for c in 0 .. (m-a-b) do
yield [|a;b;c;m-a-b-c|] }
As always, let me know in the comments how you would have tackled this problem.