Powered by AppSignal & Oban Pro

Advent of Code - Day 15: Chiton

lib/advent_of_code/2021/day-15.livemd

Advent of Code - Day 15: Chiton

Mix.install([:kino])

Day 15: Chiton

Part One: Prompt

You've almost reached the exit of the cave, but the walls are getting closer together. Your submarine can barely still fit, though; the main problem is that the walls of the cave are covered in chitons, and it would be best not to bump any of them.

The cavern is large, but has a very low ceiling, restricting your motion to two dimensions. The shape of the cavern resembles a square; a quick scan of chiton density produces a map of risk level throughout the cave (your puzzle input). For example:

1163751742
1381373672
2136511328
3694931569
7463417111
1319128137
1359912421
3125421639
1293138521
2311944581

You start in the top left position, your destination is the bottom right position, and you cannot move diagonally. The number at each position is its risk level; to determine the total risk of an entire path, add up the risk levels of each position you enter (that is, don't count the risk level of your starting position unless you enter it; leaving it adds no risk to your total).

Your goal is to find a path with the lowest total risk. In this example, a path with the lowest total risk is highlighted here:

1---------
1---------
2136511---
------15--
-------1--
-------13-
--------2-
--------3-
--------21
---------1

The total risk of this path is 40 (the starting position is never entered, so its risk is not counted).

What is the lowest total risk of any path from the top left to the bottom right?

To begin, get your puzzle input.

Part One: Solution

input = Kino.Input.textarea("Please paste your input file:")
lines = input |> Kino.Input.read() |> String.split("\n", trim: true)
["6489887186259823155119378351872148719812918213519649577918412241959623221389974192429139298912991367",
 "3859891261917894749518659872199265294116953513169391165342899913878588561865553734597561693917577259",
 "7821697449796196228941887781377133998193483644892699199569294366179448944296919898443894894653982299",
 "4521784638899152252884535888651941499199556787174181866459996191939865591888468793671399848719386874",
 "9191996931799877154649198443239834219617959491499995891819688991189512152374148611392199689771321748",
 "3667131999966376698925485914885119653598637984789199249717987982323518981692239229913697228977769187",
 "5913911179874379588135157927159887689932919711124715316555782223929991889593659797141392198858861889",
 "8889299854191893897698362569935143896411522526753243281794876746461747369943293918696398639953911511",
 "6463567347538215129956611194361438953389822419896312246999991693527638996861829479689711567997995192",
 "4969812254996785971152245859891328871698554194613499811274874939279976912834954128868111913733497634",
 "8715226296411979999175491771891518398174152999617899176858298421743955979312992148338161118133772495",
 "7989997198451591732186779418728991818771799995794999176147971519698195299959679496494381395415153939",
 "7282692814783299921331798173611516298898794899725789859192132379219117118492499969761658218929927864",
 "1342992871132334679195998919715589998911217792694649176999976859878194946915717791192969628795921896",
 "1198594318116525819324277961659629619194282188583741151135327827985988397249852849998274177979884659",
 "2948759488457996218881696122961499228919527561398512991239279917671118964912157265623911892599513341",
 "1721619411197179647114339866313852894794263785891972697189216816957987796466513815441999997333837393",
 "3375834591789764977977935817192389195669288849247457436183299942397325166499195293613453415691119331",
 "3383251379988789959621411392916296699995912197528691115352699418473557811621889429574876824981465367",
 "3946625971993126978662721165287737699957111997461919135155999927868159654568599845934641519931184857",
 "1814219784816784397613259195177424337829849136888218979928947895987954996239299692997891322152598969",
 "9167118929432986369621459985116955896895789531924573998849117693222897796967477579879164869645691987",
 "9993931541269456449491677911123286988879389825977278399128481297719892848811719696399852796988151918",
 "9893884365787225284723759349718218236629959279191199596535127988933911516948618826618861995591558212",
 "9984997699594184215439998946186347579842727156615585749955918144359797292171113989999655891195231226",
 "6519569284634811452859581242533679729318722199188497578918759345212444219469537986694271952662894183",
 "1673153584676797169319899939187899839683837996566397291171477988821999223881914917719626289955111858",
 "7146537276973579271975991138698798718183139181834274738682599352532928589621298131971429319935464919",
 "2911336366179694233796282699294517727951719996114927296729974248127291213748482125674817329679322278",
 "4798699184439539972938181916719984885811953923118182789714537319491196255791897212198915189411912489",
 "7852118738919181535168449598885759915381876297123897699578914948274581139887226879962581681998894469",
 "2918667954587166379414138982984796566391888998953829743959158938492499153199913868516322211627813911",
 "1911191198236791992999514989515915961526982494489934936648985927464538438395828491299233338941422537",
 "9861519197351334579955861514693812489919794188373831563893523762239644661783993119958349691911885978",
 "1741796188248628177931939945714851283262278358937917679382912284585329923948919181599981922219159783",
 "1918269993688698189196826775213217961928471115358414161749125718999932737938991944829186798198969188",
 "6335968599954929332975183965197971948118259199311892138919168279994983228145986116959153969714994791",
 "2294996932925629138737697471187317259977389438529157681417294638795539161967894892297711499619598914",
 "8621919128831831818711188658183498495415179256744899519992376961651631159967999793957467811269819598",
 "3486228927949465647719757181978926928894916813256577694958591299649458467861486232122397593137169986",
 "9195326888839115399949459765722899699729351261768612954948887622916193153198249712582629784163892915",
 "4946191927925146992288978981127654497969526715961851746313929219282191751297476541666118492999823964",
 "6687919199698953659176799913987999589192872822569891946679824579135968776766721961199575772548589916",
 "4134837613838699992499794185936511677831523879464926397999598699218495659219951997241941199797561898",
 "6261448956192268429191952813481184164439771317735498973672159429878683873947826158193518449579369192",
 "9968869557217691155988975292999388917423624394882599979257399494723919317766653218239966399782187116",
 "9966829587494189799987695999513479929199725278387418789286142795498925915831139896844459884251964868",
 "5396855967999263847161618545818977839821986999731717195938618127272182989829788669792191979169842532",
 "7411292996999993111239118429943655151416518359136297412488899595883341818989997121211198297187911377",
 "9196629191657399132973598379658919935439429224971517198796197371799969145169368359376974781195691986",
 ...]
max_x = lines |> List.first() |> String.length()
max_y = lines |> length()
100
grid =
  lines
  |> Enum.with_index(1)
  |> Enum.reduce(%{}, fn {row, y}, acc ->
    row
    |> String.split("", trim: true)
    |> Enum.with_index(1)
    |> Enum.into(acc, fn {col, x} -> {{x, y}, String.to_integer(col)} end)
  end)

{max_x, max_y} = max_point = Enum.max(Map.keys(grid))
{100, 100}

As we navigate, let's keep track of each point we've come to, and what the smallest total (and path taken) was to get to that point.

If we're ever on a path that comes to a point that has already been seen by a smaller total, let's drop the path we're on.

defmodule Queue do
  def add([{lowest_weight, _} | _] = queue, xy, weight) when weight <= lowest_weight do
    [{weight, xy} | queue]
  end

  def add([head | tail], xy, weight) do
    [head | add(tail, xy, weight)]
  end

  def add([], xy, weight) do
    [{weight, xy}]
  end
end

defmodule Grid do
  def print(grid) do
    {max_x, max_y} = Enum.max(Map.keys(grid))

    for y <- 1..max_y do
      1..max_x
      |> Enum.map(fn x -> Map.get(grid, {x, y}) end)
      |> Enum.join()
      |> IO.puts()
    end

    nil
  end
end

defmodule Mover do
  def navigate({map, queue}, grid) do
    if Enum.empty?(queue), do: map, else: do_navigate({map, queue}, grid)
  end

  defp do_navigate({map, [{_, point} | queue]}, grid) do
    {map, queue}
    |> move(grid, point, :up)
    |> move(grid, point, :down)
    |> move(grid, point, :left)
    |> move(grid, point, :right)
    |> navigate(grid)
  end

  defp move({%{min: {_, y}} = map, queue}, _, {_, y}, :up), do: {map, queue}
  defp move({%{max: {_, y}} = map, queue}, _, {_, y}, :down), do: {map, queue}
  defp move({%{min: {x, _}} = map, queue}, _, {x, _}, :left), do: {map, queue}
  defp move({%{max: {x, _}} = map, queue}, _, {x, _}, :right), do: {map, queue}

  defp move({map, queue}, grid, point, direction) do
    new_point = next_point(point, direction)
    sum = Map.get(map, point) + Map.get(grid, new_point)

    do_move({map, queue}, new_point, sum, Map.get(map, new_point))
  end

  defp do_move({map, queue}, point, sum, old_sum) when is_nil(old_sum) or old_sum > sum do
    {
      Map.put(map, point, sum),
      Queue.add(queue, point, sum)
    }
  end

  defp do_move(map, _, _, _), do: map

  defp next_point({x, y}, :up), do: {x, y - 1}
  defp next_point({x, y}, :down), do: {x, y + 1}
  defp next_point({x, y}, :left), do: {x - 1, y}
  defp next_point({x, y}, :right), do: {x + 1, y}
end
{:module, Mover, <<70, 79, 82, 49, 0, 0, 14, ...>>, {:next_point, 2}}
map = %{
  :min => {1, 1},
  :max => max_point,
  {1, 1} => 0
}

queue = Queue.add([], {1, 1}, 0)

{map, queue}
|> Mover.navigate(grid)
|> Map.get(max_point)
609

What is the lowest total risk of any path from the top left to the bottom right?

Your puzzle answer was 609.

The first half of this puzzle is complete! It provides one gold star: *

Part Two: Prompt

Now that you know how to find low-risk paths in the cave, you can try to find your way out.

The entire cave is actually five times larger in both dimensions than you thought; the area you originally scanned is just one tile in a 5x5 tile area that forms the full map. Your original map tile repeats to the right and downward; each time the tile repeats to the right or downward, all of its risk levels are 1 higher than the tile immediately up or left of it. However, risk levels above 9 wrap back around to 1. So, if your original map had some position with a risk level of 8, then that same position on each of the 25 total tiles would be as follows:

8 9 1 2 3
9 1 2 3 4
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7

Each single digit above corresponds to the example position with a value of 8 on the top-left tile. Because the full map is actually five times larger in both dimensions, that position appears a total of 25 times, once in each duplicated tile, with the values shown above.

Equipped with the full map, you can now find a path from the top left corner to the bottom right corner with the lowest total risk.

Using the full map, what is the lowest total risk of any path from the top left to the bottom right?

Part Two: Solution

# Increment grid 5 times in each direction
new_value = fn value, incr ->
  case rem(value + incr, 9) do
    0 -> 9
    r -> r
  end
end

big_grid =
  Enum.reduce(grid, %{}, fn
    {{x, y}, value}, big_grid ->
      for x_inc <- 0..4, y_inc <- 0..4, reduce: big_grid do
        acc ->
          Map.put(
            acc,
            {x + max_x * x_inc, y + max_y * y_inc},
            new_value.(value, x_inc + y_inc)
          )
      end
  end)

{big_max_x, big_max_y} = max_point = Enum.max(Map.keys(big_grid))
{500, 500}
# Grid.print(big_grid)
nil
map = %{
  :min => {1, 1},
  :max => max_point,
  {1, 1} => 0
}

queue = Queue.add([], {1, 1}, 0)

{map, queue}
|> Mover.navigate(big_grid)
|> Map.get(max_point)
2925

Using the full map, what is the lowest total risk of any path from the top left to the bottom right?

Your puzzle answer was 2925.

Both parts of this puzzle are complete! They provide two gold stars: **

At this point, you should return to your Advent calendar and try another puzzle.

If you still want to see it, you can get your puzzle input.