Day 23 Parts 1 and 2

This commit is contained in:
Adam Millerchip 2020-12-25 17:57:30 +09:00
parent 2786c71627
commit c9e6f4bf97
4 changed files with 182 additions and 1 deletions

View File

@ -2,7 +2,7 @@
My (attempted) solutions to [Advent of Code 2020](https://adventofcode.com/2020) in Elixir.
<img width="982" alt="image" src="https://user-images.githubusercontent.com/498229/103125884-e8a16d00-46cf-11eb-854d-67e600374e82.png">
<img width="978" alt="image" src="https://user-images.githubusercontent.com/498229/103139031-545cf780-471b-11eb-9c94-95c49a1391ba.png">
## Strategy

52
day23/README Normal file
View File

@ -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).

47
day23/day23part1.exs Normal file
View File

@ -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()

82
day23/day23part2.exs Normal file
View File

@ -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()