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

🎄 Year 2023 🔔 Day 19

elixir/notebooks/2023/day19.livemd

🎄 Year 2023 🔔 Day 19

Setup

defmodule Helpers do
  @rule_regex ~r/(?\w)(?[<>])(?\d+):(?\w+)/

  def parse_rule(rule_string) do
    %{
      "attribute" => attribute,
      "symbol" => symbol,
      "number" => number,
      "destination" => destination
    } = Regex.named_captures(@rule_regex, rule_string)

    {attribute, symbol, String.to_integer(number), destination}
  end
end

[workflows, parts] =
  File.read!("#{__DIR__}/../../../inputs/2023/day19.txt")
  |> String.split("\n\n", trim: true)

workflows =
  workflows
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    %{
      "name" => name,
      "rules" => rules,
      "final" => final_destination
    } = Regex.named_captures(~r/^(?\w+)\{(?.+),(?\w+)\}$/, line)

    rules = String.split(rules, ",") |> Enum.map(&amp;Helpers.parse_rule/1)
    {name, {rules, final_destination}}
  end)
  |> Map.new()

parts =
  parts
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    Regex.named_captures(~r/^\{x=(?\d+),m=(?\d+),a=(?\d+),s=(?\d+)\}$/, line)
    |> Enum.map(fn {k, v} -> {k, String.to_integer(v)} end)
    |> Map.new()
  end)

Part 1

defmodule Part1 do
  def execute_workflow(part, workflows, current_workflow \\ "in")
  def execute_workflow(part, _workflows, "A"), do: {part, "A"}
  def execute_workflow(part, _workflows, "R"), do: {part, "R"}

  def execute_workflow(part, workflows, current_workflow) do
    {steps, default_destination} = Map.fetch!(workflows, current_workflow)

    next_workflow =
      Enum.find_value(
        steps,
        default_destination,
        fn
          {attribute, ">", number, destination} ->
            if Map.fetch!(part, attribute) > number, do: destination, else: nil

          {attribute, "<", number, destination} ->
            if Map.fetch!(part, attribute) < number, do: destination, else: nil
        end
      )

    execute_workflow(part, workflows, next_workflow)
  end
end

Enum.map(parts, &amp;Part1.execute_workflow(&amp;1, workflows))
|> Enum.filter(fn {_part, result} -> result == "A" end)
|> Enum.flat_map(fn {part, "A"} -> Enum.map(part, &amp;elem(&amp;1, 1)) end)
|> Enum.sum()

Part 2

defmodule Part2 do
  defp split_parts(parts, {attribute, ">", number, destination}) do
    first..last = Map.fetch!(parts, attribute)

    cond do
      # Fully includes
      first > number ->
        {{parts, destination}, nil}

      # Split
      first <= number &amp;&amp; last > number ->
        included = Map.update!(parts, attribute, fn _ -> (number + 1)..last end)
        excluded = Map.update!(parts, attribute, fn _ -> first..number end)
        {{included, destination}, excluded}

      # Fully excludes
      last <= number ->
        {nil, parts}
    end
  end

  defp split_parts(parts, {attribute, "<", number, destination}) do
    first..last = Map.fetch!(parts, attribute)

    cond do
      # Fully includes
      last < number ->
        {{parts, destination}, nil}

      # Split
      first < number &amp;&amp; last >= number ->
        included = Map.update!(parts, attribute, fn _ -> first..(number - 1) end)
        excluded = Map.update!(parts, attribute, fn _ -> number..last end)
        {{included, destination}, excluded}

      # Fully excludes
      first >= number ->
        {nil, parts}
    end
  end

  defp execute_steps(parts, steps, default_destination)
  defp execute_steps(parts, [], default_destination), do: [{parts, default_destination}]

  defp execute_steps(parts, [step | steps], default_destination) do
    case split_parts(parts, step) do
      {result, nil} ->
        [result]

      {nil, remaining_parts} ->
        execute_steps(remaining_parts, steps, default_destination)

      {result, remaining_parts} ->
        [result | execute_steps(remaining_parts, steps, default_destination)]
    end
  end

  def execute_workflow(parts_with_workflow, workflows)
  def execute_workflow({parts, "A"}, _workflows), do: [{parts, "A"}]
  def execute_workflow({parts, "R"}, _workflows), do: [{parts, "R"}]

  def execute_workflow({parts, current_workflow}, workflows) do
    {steps, default_destination} = Map.fetch!(workflows, current_workflow)

    execute_steps(parts, steps, default_destination)
  end
end

[
  {%{
     "x" => 1..4000,
     "m" => 1..4000,
     "a" => 1..4000,
     "s" => 1..4000
   }, "in"}
]
|> Stream.iterate(fn elements ->
  Enum.flat_map(elements, &amp;Part2.execute_workflow(&amp;1, workflows))
end)
|> Enum.find(fn elements ->
  Enum.all?(elements, fn {_parts, current_workflow} ->
    current_workflow == "A" || current_workflow == "R"
  end)
end)
|> Enum.filter(fn {_parts, current_workflow} -> current_workflow == "A" end)
|> Enum.map(fn {parts, "A"} ->
  parts
  |> Enum.map(fn {_attribute, range} -> Enum.count(range) end)
  |> Enum.product()
end)
|> Enum.sum()