diff --git a/README.md b/README.md index 3cf6de6..500d52e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ My (attempted) solutions to [Advent of Code 2020](https://adventofcode.com/2020) in Elixir. -image +image ## Strategy diff --git a/day23/README b/day23/README new file mode 100644 index 0000000..6acf3bb --- /dev/null +++ b/day23/README @@ -0,0 +1,52 @@ +Day 23 Notes + ++--------+ +| Part 1 | ++--------+ + +$ elixir day23part1.exs +59374826 + +Thoughts: + +Need to represent a circle. +Use a combination of lists, and Stream.cycle to handle the wrap-around. + ++--------+ +| Part 2 | ++--------+ + +$ elixir day23part2.exs +Next two cups: 266262, 251174 +Answer: 66878091588 + +Thoughts: + +Considering the numbers involved (1M items, 10M iterations), the Part 1 solution with its +O(n) list operations is just not going to work. We need a solution that works in constant time. + +How to represent the circle: we need a doubly-linked list with random access *in constant time*. +Two options I can think of: Erlang's :array, or :ets. :array's documentation doesn't say anything +about its performance and the arrays seem resizable, however :ets says it's constant time, so I +decided to use ETS. + +Completely re-write the part 1 implementation. Represent the circle in ETS with the keys being the +cup label, and the values being a {prev, next} tuple. + +Remove from the circle: Update the two new neighbours to point to each other. +Add to the circle: Update the four new neighbours (left & right extremities) to point to each +other. + +Even after these efficiency improvements, this still takes 22 seconds on my machine >.< + ++------------------+ +| Overall Thoughts | ++------------------+ + +Interesting problem. Produced an elegant solution for Part 1 that simply wasn't scalable for Part +2. Part 2 required working around the beam's limitations of number-crunching a large dataset. ETS +kind of worked, but was still very slow, so this is obviously not scalable. I will be interested +to see if people came up with a solution that completes in less than a second (or even 10 seconds, +as per the guideline). + + diff --git a/day23/day23part1.exs b/day23/day23part1.exs new file mode 100644 index 0000000..c2131ea --- /dev/null +++ b/day23/day23part1.exs @@ -0,0 +1,47 @@ +defmodule Day23Part1 do + # @input "389125467" + @input "137826495" + def run do + @input + |> String.graphemes() + |> Enum.map(&String.to_integer/1) + |> play(100) + |> cycle_to(1) + |> Enum.drop(1) + |> Enum.join() + |> IO.puts() + end + + def play(cups, 0), do: cups + def play(cups, times), do: move(cups) |> play(times - 1) + + def move([current, a, b, c | rest]) do + holding = [a, b, c] + dest = find_dest(current, holding) + + insert(holding, dest, [current | rest]) + |> cycle_to(current, 1) + end + + def find_dest(1, holding), do: find_dest(10, holding) + + def find_dest(current, holding) do + dest = current - 1 + if dest in holding, do: find_dest(dest, holding), else: dest + end + + def insert(holding, dest, circle) do + [dest | rest] = cycle_to(circle, dest) + [dest | holding] ++ rest + end + + def cycle_to(circle, target, offset \\ 0) do + circle + |> Stream.cycle() + |> Stream.drop_while(&(&1 != target)) + |> Stream.take(length(circle) + offset) + |> Enum.drop(offset) + end +end + +Day23Part1.run() diff --git a/day23/day23part2.exs b/day23/day23part2.exs new file mode 100644 index 0000000..4582acc --- /dev/null +++ b/day23/day23part2.exs @@ -0,0 +1,82 @@ +defmodule Day23Part2 do + @input "137826495" + def run do + {a, b} = + @input + |> String.graphemes() + |> Enum.map(&String.to_integer/1) + |> init_cups() + |> play(10_000_000) + |> next_two_after_1() + + IO.puts("Next two cups: #{a}, #{b}") + IO.puts("Answer: #{a * b}") + end + + def init_cups([first, second | _] = input) do + cups = :ets.new(:circle, []) + + (input ++ for(i <- 10..1_000_000, do: i)) + |> Stream.chunk_every(3, 1) + |> Stream.each(fn + [a, b, c] -> + :ets.insert(cups, {b, {a, c}}) + + [penultimate, last] -> + :ets.insert(cups, {first, {last, second}}) + :ets.insert(cups, {last, {penultimate, first}}) + end) + |> Stream.run() + + {first, cups} + end + + def play({_current, cups}, 0), do: cups + def play(state, times), do: move(state) |> play(times - 1) + + def move({current, cups}) do + holding = take_three(cups, current) + dest = find_dest(holding, current) + cups = insert(holding, dest, cups) + [{^current, {_prev, next}}] = :ets.lookup(cups, current) + + {next, cups} + end + + def find_dest(holding, 1), do: find_dest(holding, 1_000_001) + + def find_dest(holding, current) do + dest = current - 1 + if dest in holding, do: find_dest(holding, dest), else: dest + end + + def take_three(cups, current) do + [{^current, {prev, a}}] = :ets.lookup(cups, current) + [{^a, {^current, b}}] = :ets.lookup(cups, a) + [{^b, {^a, c}}] = :ets.lookup(cups, b) + [{^c, {^b, next}}] = :ets.lookup(cups, c) + [{^next, {^c, last}}] = :ets.lookup(cups, next) + :ets.insert(cups, {current, {prev, next}}) + :ets.insert(cups, {next, {current, last}}) + [a, b, c] + end + + def insert([a, b, c], dest, cups) do + [{^dest, {prev, next}}] = :ets.lookup(cups, dest) + [{^next, {^dest, last}}] = :ets.lookup(cups, next) + :ets.insert(cups, {dest, {prev, a}}) + :ets.insert(cups, {a, {dest, b}}) + :ets.insert(cups, {b, {a, c}}) + :ets.insert(cups, {c, {b, next}}) + :ets.insert(cups, {next, {c, last}}) + cups + end + + def next_two_after_1(cups) do + [{1, {_prev, next}}] = :ets.lookup(cups, 1) + [{^next, {1, nextnext}}] = :ets.lookup(cups, next) + {next, nextnext} + end +end + +Day23Part2.run()