Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code - Day 9

livebooks/day9.livemd

Advent of Code - Day 9

Part 1

raw_sample = "2333133121414131402"

A disk map like 12345 would represent a one-block file, two blocks of free space, a three-block file, four blocks of free space, and then a five-block file. A disk map like 90909 would represent three nine-block files in a row (with no free space between them).

Each file on disk also has an ID number based on the order of the files as they appear before they are rearranged, starting with ID 0. So, the disk map 12345 has three files: a one-block file with ID 0, a three-block file with ID 1, and a five-block file with ID 2. Using one character for each block where digits are the file ID and . is free space, the disk map 12345 represents these individual blocks:

0..111….22222 The first example above, 2333133121414131402, represents these individual blocks:

00…111…2…333.44.5555.6666.777.888899

The amphipod would like to move file blocks one at a time from the end of the disk to the leftmost free space block (until there are no gaps remaining between file blocks). For the disk map 12345, the process looks like this:

0..111….22222 02.111….2222. 022111….222.. 0221112…22… 02211122..2…. 022111222……

Continuing the first example, the first few blocks’ position multiplied by its file ID number are 0 0 = 0, 1 0 = 0, 2 9 = 18, 3 9 = 27, 4 * 8 = 32, and so on. In this example, the checksum is the sum of these, 1928.

Compact the amphipod’s hard drive using the process he requested. What is the resulting filesystem checksum?

{_, list} = raw_sample
|> String.graphemes()
|> Enum.with_index()
|> Enum.reduce({0, []}, fn {block_size, i}, {acc, list} ->
  block_size = String.to_integer(block_size)
  case rem(i, 2) do
    0 ->
      {acc + 1, [List.duplicate(acc, block_size) | list]}

    1 ->
      # free space
      {acc, [List.duplicate(".", block_size) | list]}
      end
end)
list
|> Enum.reverse()
|> Enum.flat_map(fn x -> x end)
flat_list = list
|> Enum.reverse()
|> Enum.flat_map(fn x -> x end)

map = flat_list
|> Enum.with_index()
|> Enum.reduce(%{}, fn {e, i}, map ->
  Map.put(map, i, e)
end)
defmodule Part1 do
  def leftmost_free_space(map, start) do
    Stream.iterate(start, &(&1 + 1))
    |> Enum.find_value(fn i ->
      case Map.get(map, i) do
        nil -> -1
        "." -> i
        _ -> false
      end
    end)
  end

  def rightmost_data_block(map, start) do
    start..0//-1
    |> Enum.find_value(fn i ->
      case Map.get(map, i) do
        nil -> -1
        "." -> false
        _ -> i
      end
    end)
  end

  def parse_input(input) do
    {_, list} =
      input
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce({0, []}, fn {block_size, i}, {acc, list} ->
        block_size = String.to_integer(block_size)

        case rem(i, 2) do
          0 ->
            {acc + 1, [List.duplicate(acc, block_size) | list]}

          1 ->
            # free space
            {acc, [List.duplicate(".", block_size) | list]}
        end
      end)

    list
    |> Enum.reverse()
    |> Enum.flat_map(fn x -> x end)
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {e, i}, map ->
      Map.put(map, i, e)
    end)
  end

  def solve(map) do
    size = map |> Map.keys() |> length()

    recurse(map, 0, size - 1)
  end

  def find_sum(map) do
    map
    |> Enum.reduce(0, fn {k, v}, acc ->
      case v do
        "." -> acc
        _ -> acc + k * v
      end
    end)
  end

  def recurse(map, left, right) when left >= right do
    map
  end

  def recurse(map, left, right) do
    le = Map.fetch!(map, left)
    re = Map.fetch!(map, right)

    case le do
      "." ->
        case re do
          "." ->
            right = rightmost_data_block(map, right - 1)
            case right do
              nil -> map
              _ -> recurse(map, left, right)
            end      
          _ ->
            map =
              Map.put(map, left, re)
              |> Map.put(right, le)

            recurse(map, left + 1, right - 1)
        end

      _ ->
        left = leftmost_free_space(map, left + 1)
        case left do
          nil -> map
          _ -> recurse(map, left, right)
        end
    end
  end
end
Part1.parse_input(raw_sample)
|> Part1.solve()
|> dbg()
|> Part1.find_sum()
File.read!("/Users/errantsky/elixir-projects/phoenix-projects/advent_of_code_2024/inputs/day9.txt")
|> String.trim("\n")
|> String.trim()
|> Part1.parse_input()
|> Part1.solve()
|> dbg()
|> Part1.find_sum()

Part 2

Attempt to move whole files to the leftmost span of free space blocks that could fit the file. Attempt to move each file exactly once in order of decreasing file ID number starting with the file with the highest file ID number. If there is no span of free space to the left of a file that is large enough to fit the file, the file does not move.

The first example from above now proceeds differently:

00...111...2...333.44.5555.6666.777.888899
0099.111...2...333.44.5555.6666.777.8888..
0099.1117772...333.44.5555.6666.....8888..
0099.111777244.333....5555.6666.....8888..
00992111777.44.333....5555.6666.....8888..

The process of updating the filesystem checksum is the same; now, this example’s checksum would be 2858.

defmodule Part2 do
  def leftmost_free_space(map, block_size) do
    Stream.iterate(0, &(&1 + 1))
    |> Enum.find_value(fn i ->
      case Map.get(map, i) do
        nil -> -1
        {:free, nil, free_size} -> 
          if free_size >= block_size do
            i
          else
            false
          end
        _ -> false
      end
    end)
  end

  def rightmost_data_block(map, start) do
    start..0//-1
    |> Enum.find_value(fn i ->
      case Map.get(map, i) do
        nil -> -1
        {:free, nil, _free_size} -> false
        _ -> i
      end
    end)
  end

  def parse_input(input) do
    {_, list} =
      input
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce({0, []}, fn {block_size, i}, {acc, list} ->
        block_size = String.to_integer(block_size)

        case rem(i, 2) do
          0 ->
            {acc + 1, [{:data, acc, block_size} | list]}
          1 ->
            {acc, [{:free, nil, block_size} | list]}
        end
      end)

    list
    |> Enum.reverse()
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {t, idx}, map -> Map.put(map, idx, t) end)
  end

  def solve(map) do
    size = map |> Map.keys() |> length()

    recurse(map, 0, size - 1)
  end

  def find_sum(map) do
    map
    |> Enum.reduce(0, fn {k, v}, acc ->
      case v do
        "." -> acc
        _ -> acc + k * v
      end
    end)
  end

  def recurse(map, left, right) when left >= right do
    map
  end

  def recurse(map, left, right) do
    le = Map.fetch!(map, left)
    re = Map.fetch!(map, right)

    case le do
      "." ->
        case re do
          "." ->
            right = rightmost_data_block(map, right - 1)
            case right do
              nil -> map
              _ -> recurse(map, left, right)
            end      
          _ ->
            {_, nil, free_size} = left
            {_, idx, block_size} = right
            leftover_free_size = free_size - block_size

            # update free index to point to block
            # if leftover more than 0
            # update the next block with the leftover free space
            # shift everything to the right

            # need to account for leftover free space
            # Might need to reset the left and right
            

            recurse(map, left + 1, right - 1)
        end

      _ ->
        left = leftmost_free_space(map, right |> Enum.at(2))
        case left do
          nil -> map
          _ -> recurse(map, left, right)
        end
    end
  end
end
raw_sample
Part2.parse_input(raw_sample)
Part2.parse_input(raw_sample)
|> Part2.leftmost_free_space(3)
raw_sample |> String.graphemes() |> length()
Part2.parse_input(raw_sample)
|> Part2.rightmost_data_block(raw_sample |> String.graphemes() |> length() |> Kernel.-(1))