Day 19: Aplenty
by @mbovel
Puzzle description
https://adventofcode.com/2023/day/19
Data structures
We define the following data structures for today's puzzle:
enum Channel:
  case X, M, A, S
enum Operator:
  case LessThan, GreaterThan
enum Result:
  case Reject, Accept
enum Instruction:
  case IfThenElse(
      channel: Channel,
      operator: Operator,
      value: Int,
      thenBranch: GoTo | Return,
      elseBranch: Instruction
  )
  case Return(result: Result)
  case GoTo(target: String)
import Instruction.*
type Workflow = Map[String, Instruction]
case class Part(x: Int, m: Int, a: Int, s: Int)
Parsing
Then, we write the associated parsing methods, using String's split method and regular expression patterns
object Channel:
  def parse(input: String): Channel =
    input match
      case "x" => Channel.X
      case "m" => Channel.M
      case "a" => Channel.A
      case "s" => Channel.S
      case _   => throw Exception(s"Invalid channel: $input")
object Operator:
  def parse(input: String): Operator =
    input match
      case "<" => Operator.LessThan
      case ">" => Operator.GreaterThan
      case _   => throw Exception(s"Invalid operator: $input")
object Result:
  def parse(input: String): Result =
    input match
      case "R" => Result.Reject
      case "A" => Result.Accept
      case _   => throw Exception(s"Invalid result: $input")
object Instruction:
  private val IfThenElseRegex = """([xmas])([<>])(\d+):(\w+),(.*)""".r
  private val ReturnRegex = """([RA])""".r
  private val GoToRegex = """(\w+)""".r
  def parse(input: String): Instruction =
    input match
      case IfThenElseRegex(channel, operator, value, thenBranch, elseBranch) =>
        Instruction.parse(thenBranch) match
          case thenBranch: (GoTo | Return) =>
            IfThenElse(
              Channel.parse(channel),
              Operator.parse(operator),
              value.toInt,
              thenBranch,
              Instruction.parse(elseBranch)
            )
          case _ => throw Exception(s"Invalid then branch: $thenBranch")
      case ReturnRegex(result) => Return(Result.parse(result))
      case GoToRegex(target)   => GoTo(target)
      case _ => throw Exception(s"Invalid instruction: $input")
object Workflow:
  def parse(input: String): Workflow =
    input.split("\n").map(parseBlock).toMap
  private val BlockRegex = """(\w+)\{(.*?)\}""".r
  private def parseBlock(input: String): (String, Instruction) =
    input match
      case BlockRegex(label, body) =>
        (label, Instruction.parse(body))
object Part:
  val PartRegex = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}""".r
  def parse(input: String): Part =
    input match
      case PartRegex(x, m, a, s) => Part(x.toInt, m.toInt, a.toInt, s.toInt)
      case _                     => throw Exception(s"Invalid part: $input")
Part 1 – Evaluation
These helpers allow us to implement the core logic succinctly:
def part1(input: String): Int =
  val Array(workflowLines, partLines) = input.split("\n\n")
  val workflow = Workflow.parse(workflowLines.trim())
  val parts = partLines.trim().split("\n").map(Part.parse)
  def eval(part: Part, instruction: Instruction): Result =
    instruction match
      case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
        val channelValue = channel match
          case Channel.X => part.x
          case Channel.M => part.m
          case Channel.A => part.a
          case Channel.S => part.s
        val result = operator match
          case Operator.LessThan    => channelValue < value
          case Operator.GreaterThan => channelValue > value
        if result then eval(part, thenBranch) else eval(part, elseBranch)
      case Return(result) => result
      case GoTo(target)   => eval(part, workflow(target))
  parts
    .collect: part =>
      eval(part, workflow("in")) match
        case Result.Reject => 0
        case Result.Accept => part.x + part.m + part.a + part.s
    .sum
Part 2 – Symbolic execution
To solve the second part efficiently, we use symbolic execution to count the number of executions of the workflow that lead to an Accept result.
We represent symbolic values with the AbstractPart structure, where the value associated to each channel is not a number, but a range of possible values:
case class Range(from: Long, until: Long):
  assert(from < until)
  def count() = until - from
object Range:
  def safe(from: Long, until: Long): Option[Range] =
    if from < until then Some(Range(from, until)) else None
case class AbstractPart(x: Range, m: Range, a: Range, s: Range):
  def count() = x.count() * m.count() * a.count() * s.count()
  def withChannel(channel: Channel, newRange: Range) =
    channel match
      case Channel.X => copy(x = newRange)
      case Channel.M => copy(m = newRange)
      case Channel.A => copy(a = newRange)
      case Channel.S => copy(s = newRange)
  def getChannel(channel: Channel) =
    channel match
      case Channel.X => x
      case Channel.M => m
      case Channel.A => a
      case Channel.S => s
We will start the evaluation with abstract parts that contain all possible values for each channel: four ranges from 1 until 4001.
When we evaluate an IfThenElse instruction, we split the argument AbstractPart into two parts, one that contains only the values that satisfy the condition, and one that contains only the values that do not satisfy the condition.
This is achieved with the split method of AbstractPart:
  def split(
      channel: Channel,
      value: Int
  ): (Option[AbstractPart], Option[AbstractPart]) =
    val currentRange = getChannel(channel)
    (
      Range.safe(currentRange.from, value).map(withChannel(channel, _)),
      Range.safe(value, currentRange.until).map(withChannel(channel, _))
    )
Using these helpers, we can implement the abstract evaluator as follows:
def part2(input: String): Long = combinations(input, 4001)
def combinations(input: String, until: Long): Long =
  val Array(workflowLines, _) = input.split("\n\n")
  val workflow = Workflow.parse(workflowLines.trim())
  def count(part: AbstractPart, instruction: Instruction): Long =
    instruction match
      case IfThenElse(channel, operator, value, thenBranch, elseBranch) =>
        val (trueValues, falseValues) =
          operator match
            case Operator.LessThan    => part.split(channel, value)
            case Operator.GreaterThan => part.split(channel, value + 1).swap
        trueValues.map(count(_, thenBranch)).getOrElse(0L)
          + falseValues.map(count(_, elseBranch)).getOrElse(0L)
      case Return(Result.Accept) => part.count()
      case Return(Result.Reject) => 0L
      case GoTo(target)          => count(part, workflow(target))
  count(
    AbstractPart(
      Range(1, until),
      Range(1, until),
      Range(1, until),
      Range(1, until)
    ),
    workflow("in")
  )
Solutions from the community
- Solution by merlin
- Solution by jnclt
- Solution by Raymond Dodge
- Solution by Spamegg
- Solution by Rui Alves
- Solution by g.berezin
- Solution by Jamie Thompson
- Solution by Paweł Cembaluk
Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format