Day 19: Aplenty

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

  • cacheson@kbin.social
    link
    fedilink
    arrow-up
    2
    ·
    2 years ago

    Nim

    Part 1 was pretty straightforward. For part 2 I made an ItemRange type that’s just one integer range for each attribute. I also made a split function that returns two ItemRange objects, one for the values that match the specified rule, and the others for the unmatched values. When iterating through the workflows, I start a new recursion branch to process any matching values, and continue stepping through with the unmatched values until none remain or they’re accepted/rejected.

  • Amy
    link
    fedilink
    arrow-up
    2
    ·
    2 years ago

    Haskell

    Echoes of Day 5… Not particularly difficult, but lots of typing. I’d like to golf this one a bit more.

    Solution
    import Control.Monad
    import Data.Either
    import Data.Maybe
    import Text.Parsec
    
    type Rule = (Maybe (Char, Ordering, Int), String)
    
    type Flow = (String, [Rule])
    
    type Part = [(Char, Int)]
    
    readInput :: String -> ([Flow], [Part])
    readInput s =
      let (flows, _ : parts) = break (== "") $ lines s
       in (map readFlow flows, map readPart parts)
      where
        readFlow = fromRight (error "bad flow") . parse flow ""
        readPart = fromRight (error "bad part") . parse part ""
        flow = do
          name <- many1 letter
          rules <- between (char '{') (char '}') (rule `sepBy` char ',')
          return (name, rules)
        rule = do
          c <- optionMaybe $ try condition
          n <- many1 letter
          return (c, n)
        condition = do
          p <- anyChar
          o <- choice [LT <$ char '<', GT <$ char '>']
          n <- read <$> many1 digit
          char ':' >> return (p, o, n)
        part = between (char '{') (char '}') (param `sepBy` char ',')
        param = do
          p <- anyChar
          n <- char '=' >> (read <$> many1 digit)
          return (p, n)
    
    runPart :: [Flow] -> Part -> Bool
    runPart flows part = runFlow "in"
      where
        runFlow "A" = True
        runFlow "R" = False
        runFlow f = runRules $ fromJust $ lookup f flows
        runRules ((Nothing, next) : _) = runFlow next
        runRules ((Just (p, o, n), next) : rest)
          | compare (fromJust $ lookup p part) n == o = runFlow next
          | otherwise = runRules rest
    
    mapRanges :: [Flow] -> [(Char, (Int, Int))] -> [[(Char, (Int, Int))]]
    mapRanges flows = runFlow "in"
      where
        runFlow "A" = return
        runFlow "R" = const mzero
        runFlow f = runRules (fromJust $ lookup f flows)
        runRules ((Nothing, next) : _) = runFlow next
        runRules ((Just test, next) : rest) =
          (\(a, b) -> join [a >>= runFlow next, b >>= runRules rest]) . splitRange test
        splitRange (p, op, n) range =
          let (v1, v2) = fromJust $ lookup p range
              others = filter ((/= p) . fst) range
           in case op of
                LT
                  | v1 >= n -> ([], [range])
                  | v2 < n -> ([range], [])
                  | otherwise -> ([(p, (v1, n - 1)) : others], [(p, (n, v2)) : others])
                GT
                  | v2 <= n -> ([], [range])
                  | v1 > n -> ([range], [])
                  | otherwise -> ([(p, (n + 1, v2)) : others], [(p, (v1, n)) : others])
    
    main = do
      (flows, parts) <- readInput <$> readFile "input19"
      print . sum . concatMap (map snd) $ filter (runPart flows) parts
      print $
        sum . map (product . map (\(_, (v1, v2)) -> v2 - v1 + 1)) $
          mapRanges flows [(p, (1, 4000)) | p <- "xmas"]
    
    • sjmulder
      link
      fedilink
      arrow-up
      2
      ·
      2 years ago

      That’s a nice parser and I like the use of pattern matching here.

      What I wonder is if you couldn’t make a cool monadic representation of the ranges, where you could do the equivalent of

      if (foo.x > 12)
          return bar(x);
      else
          return baz(x);
      

      but ‘x’ wouldn’t be an integer, it’d be a collection of integer ranges. The > operator would return a collection of pairs of (newly split) integer ranges and booleans. The if would yield map the pairs to a new bunch of pairs and so on.

      • Amy
        link
        fedilink
        arrow-up
        2
        ·
        2 years ago

        Mmm, I was thinking something similar. I’ve been meaning to go back and have another go, but the last few problems have eaten up all my time (and energy!)

        I realized with the parser you can write eg

            rule = (,) <$> optionMaybe (try condition) <*> many1 letter
        

        which avoids even more of those pesky variable names! (I still haven’t quite internalized how to use Applicative)

  • cvttsd2si@programming.dev
    link
    fedilink
    arrow-up
    2
    ·
    2 years ago

    Scala3

    case class Part(x: Range, m: Range, a: Range, s: Range):
        def rating: Int = x.start + m.start + a.start + s.start
        def combinations: Long = x.size.toLong * m.size.toLong * a.size.toLong * s.size.toLong
    
    type ActionFunc = Part => (Option[(Part, String)], Option[Part])
    
    case class Workflow(ops: List[ActionFunc]):
        def process(p: Part): List[(Part, String)] =
            @tailrec def go(p: Part, ops: List[ActionFunc], acc: List[(Part, String)]): List[(Part, String)] =
                ops match
                    case o :: t => o(p) match
                        case (Some(branch), Some(fwd)) => go(fwd, t, branch::acc)
                        case (None, Some(fwd)) => go(fwd, t, acc)
                        case (Some(branch), None) => branch::acc
                        case (None, None) => acc
                    case _ => acc
            go(p, ops, List())
    
    def run(parts: List[Part], workflows: Map[String, Workflow]) =
        @tailrec def go(parts: List[(Part, String)], accepted: List[Part]): List[Part] =
            parts match
                case (p, wf) :: t => 
                    val res = workflows(wf).process(p)
                    val (acc, rest) = res.partition((_, w) => w == "A")
                    val (rej, todo) = rest.partition((_, w) => w == "R")
                    go(todo ++ t, acc.map(_._1) ++ accepted)
                case _ => accepted
        go(parts.map(_ -> "in"), List())
    
    def parseWorkflows(a: List[String]): Map[String, Workflow] =
        def generateActionGt(n: Int, s: String, accessor: Part => Range, setter: (Part, Range) => Part): ActionFunc = p => 
            val r = accessor(p)
            (Option.when(r.end > n + 1)((setter(p, math.max(r.start, n + 1) until r.end), s)), Option.unless(r.start > n)(setter(p, r.start until math.min(r.end, n + 1))))
        def generateAction(n: Int, s: String, accessor: Part => Range, setter: (Part, Range) => Part): ActionFunc = p => 
            val r = accessor(p)
            (Option.when(r.start &lt; n)((setter(p, r.start until math.min(r.end, n)), s)), Option.unless(r.end &lt;= n)(setter(p, math.max(r.start, n) until r.end)))
        
        val accessors = Map("x"->((p:Part) => p.x), "m"->((p:Part) => p.m), "a"->((p:Part) => p.a), "s"->((p:Part) => p.s))
        val setters = Map("x"->((p:Part, v:Range) => p.copy(x=v)), "m"->((p:Part, v:Range) => p.copy(m=v)), "a"->((p:Part, v:Range) => p.copy(a=v)), "s"->((p:Part, v:Range) => p.copy(s=v)))
    
        def parseAction(a: String): ActionFunc =
            a match
                case s"$v&lt;$n:$s" => generateAction(n.toInt, s, accessors(v), setters(v))
                case s"$v>$n:$s" => generateActionGt(n.toInt, s, accessors(v), setters(v))
                case s => p => (Some((p, s)), None)
    
        a.map(_ match{ case s"$name{$items}" => name -> Workflow(items.split(",").map(parseAction).toList) }).toMap
    
    def parsePart(a: String): Option[Part] =
        a match
            case s"{x=$x,m=$m,a=$a,s=$s}" => Some(Part(x.toInt until 1+x.toInt, m.toInt until 1+m.toInt, a.toInt until 1+a.toInt, s.toInt until 1+s.toInt))
            case _ => None
    
    def task1(a: List[String]): Long = 
        val in = a.chunk(_ == "")
        val wfs = parseWorkflows(in(0))
        val parts = in(1).flatMap(parsePart)
        run(parts, wfs).map(_.rating).sum
    
    def task2(a: List[String]): Long =
        val wfs = parseWorkflows(a.chunk(_ == "").head)
        val parts = List(Part(1 until 4001, 1 until 4001, 1 until 4001, 1 until 4001))
        run(parts, wfs).map(_.combinations).sum
    
  • sjmulder
    link
    fedilink
    arrow-up
    2
    ·
    2 years ago

    C

    Bit of typing and testing again but part 2 was fun and not hard, although I did make a few logic mistakes.

    Didn’t optimize anything in particular but as in day 8 (iirc) I avoid dealing with strings for references to things, so I keep a string table and only use indices.

    Abridged excerpt of data structures and eval():

    struct expr { int type, var, imm, next; };
    struct partrange { int min[4], max[4]; };
    
    static struct expr flows[600][5];
    static int accept_id, reject_id, in_id;
    
    static int64_t eval(int id, struct partrange p)
    {
    	struct partrange q;
    	struct expr *e;
    	int64_t acc=0;
    	int i;
    
    	if (id == reject_id ||
    	    p.min[0] > p.max[0] || p.min[1] > p.max[1] ||
    	    p.min[2] > p.max[2] || p.min[3] > p.max[3])
    	    	return 0;
    
    	if (id == accept_id)
    		return (int64_t)
    		    (p.max[0] -p.min[0] +1) * (p.max[1] -p.min[1] +1) *
    		    (p.max[2] -p.min[2] +1) * (p.max[3] -p.min[3] +1);
    
    	for (i=0; i < (int)LEN(*flows); i++)
    		switch ((e = &flows[id][i])->type) {
    		case EXPR_LT:
    			q = p;
    			q.max[e->var] = MIN(q.max[e->var], e->imm-1);
    			p.min[e->var] = MAX(p.min[e->var], e->imm);
    			acc += eval(e->next, q);
    			break;
    		case EXPR_GT:
    			q = p;
    			q.min[e->var] = MAX(q.min[e->var], e->imm+1);
    			p.max[e->var] = MIN(p.max[e->var], e->imm);
    			acc += eval(e->next, q);
    			break;
    		case EXPR_CALL:
    			acc += eval(e->next, p);
    			return acc;
    		}
    	
    	assert(!"bad flow");
    }
    

    https://github.com/sjmulder/aoc/blob/master/2023/c/day19.c

  • Zarlin@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    2 years ago

    Nim

    I optimized Part1 by directly referencing workflows between each rule (instead of doing a table lookup between them), in expectation of part 2 needing increased performance. But that turned out to not be needed 😋

    I had to dig through my dusty statistics knowledge for part 2, and decided to try out Mermaid.js to create a little graph of the sample input to help visualize the solution.

    After that it was pretty straightforward.

    Day 19, part 1+2