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

Day 19

d19.livemd

Day 19

Solution

defmodule D19 do
  @workflow_regex ~r/(?\w+){(?.+),(?\w+)}/
  @cond_regex ~r/(?[xmas])(?[><])(?\d+):(?\w+)/
  @part_regex ~r/{x=(?\d+),m=(?\d+),a=(?\d+),s=(?\d+)}/

  def parse_input(input) do
    [workflows, parts] = input |> String.trim() |> String.split("\n\n")

    workflows =
      workflows
      |> String.split("\n")
      |> Enum.map(fn workflow ->
        %{
          "name" => name,
          "conds" => conds,
          "otherwise" => otherwise
        } = Regex.named_captures(@workflow_regex, workflow)

        conds =
          String.split(conds, ",")
          |> Enum.map(fn condition ->
            parts = Regex.named_captures(@cond_regex, condition)

            %{
              cat: parts["cat"],
              op: parts["op"],
              send_to: parts["send_to"],
              val: String.to_integer(parts["val"])
            }
          end)

        {name, %{conds: conds, otherwise: otherwise}}
      end)
      |> Enum.into(%{})

    parts =
      parts
      |> String.split("\n")
      |> Enum.map(fn part ->
        Regex.named_captures(@part_regex, part)
        |> Enum.map(fn {k, v} -> {k, String.to_integer(v)} end)
        |> Enum.into(%{})
      end)

    %{workflows: workflows, parts: parts}
  end

  def rating(part) when is_map(part), do: Map.values(part) |> Enum.sum()

  def run(part, workflows) when is_map(part) and is_map(workflows) do
    run(part, workflows, "in")
  end

  def run(_part, _workflows, "A"), do: "A"
  def run(_part, _workflows, "R"), do: "R"

  def run(part, workflows, workflow) do
    current = workflows[workflow]

    matching_condition =
      Enum.find(current.conds, fn condition ->
        part_value = part[condition.cat]

        case condition.op do
          ">" -> part_value > condition.val
          "<" -> part_value < condition.val
        end
      end)

    if is_nil(matching_condition) do
      run(part, workflows, current.otherwise)
    else
      run(part, workflows, matching_condition.send_to)
    end
  end

  def p1(input) do
    Enum.filter(input.parts, fn part ->
      run(part, input.workflows) == "A"
    end)
    |> Enum.map(&amp;rating/1)
    |> Enum.sum()
  end

  def p2(input) do
    ranges = for key <- ["x", "m", "a", "s"], do: {key, 1..4000}, into: %{}
    count_acceptable(ranges, input.workflows, "in")
  end

  def count_acceptable(_ranges, _workflows, "R"), do: 0

  def count_acceptable(ranges, _workflows, "A") do
    ranges |> Map.values() |> Enum.map(&amp;Range.size/1) |> Enum.product()
  end

  def count_acceptable(ranges, workflows, workflow) do
    current = workflows[workflow]

    {remaining, acceptable} =
      Enum.reduce(
        current.conds,
        {ranges, 0},
        fn condition, {ranges, acceptable} ->
          current_cat_range = ranges[condition.cat]

          {pass, fail} =
            case condition.op do
              ">" ->
                pass = Map.put(ranges, condition.cat, (condition.val + 1)..current_cat_range.last)
                fail = Map.put(ranges, condition.cat, current_cat_range.first..condition.val)
                {pass, fail}

              "<" ->
                pass =
                  Map.put(ranges, condition.cat, current_cat_range.first..(condition.val - 1))

                fail = Map.put(ranges, condition.cat, condition.val..current_cat_range.last)
                {pass, fail}
            end

          {fail, acceptable + count_acceptable(pass, workflows, condition.send_to)}
        end
      )

    acceptable + count_acceptable(remaining, workflows, current.otherwise)
  end
end
{:module, D19, <<70, 79, 82, 49, 0, 0, 42, ...>>, {:count_acceptable, 3}}

Tests

ExUnit.start(autorun: false)

defmodule D08Test do
  use ExUnit.Case, async: true

  @test_input D19.parse_input("""
              px{a<2006:qkq,m>2090:A,rfg}
              pv{a>1716:R,A}
              lnx{m>1548:A,A}
              rfg{s<537:gd,x>2440:R,A}
              qs{s>3448:A,lnx}
              qkq{x<1416:A,crn}
              crn{x>2662:A,R}
              in{s<1351:px,qqz}
              qqz{s>2770:qs,m<1801:hdj,R}
              gd{a>3333:R,R}
              hdj{m>838:A,pv}

              {x=787,m=2655,a=1222,s=2876}
              {x=1679,m=44,a=2067,s=496}
              {x=2036,m=264,a=79,s=2244}
              {x=2461,m=1339,a=466,s=291}
              {x=2127,m=1623,a=2188,s=1013}   
              """)
  @input Path.join(__DIR__, "inputs/d19") |> File.read!() |> D19.parse_input()

  test "part 1 works" do
    assert D19.p1(@test_input) == 19114
    assert D19.p1(@input) == 399_284
  end

  test "part 2 works" do
    assert D19.p2(@test_input) == 167_409_079_868_000
    assert D19.p2(@input) == 121_964_982_771_486
  end
end

ExUnit.run()
..
Finished in 0.00 seconds (0.00s async, 0.00s sync)
2 tests, 0 failures

Randomized with seed 140176
%{excluded: 0, failures: 0, skipped: 0, total: 2}