Skip to content

re: AoC Day 3: No Matter How You Slice It VIEW POST


I saw someone on the Clojurian's Slack talking about AoC day 3, and made the statement "mutable arrays FTW!" This made me think to try using the core.matrix library. I'm always looking for excuses to get better at using that library, since it can give direct GPU access when doing linear algebra.

Part the First

(ns day3
  (:require [clojure.string :refer [split]]
            [clojure.core.matrix :refer :all]))
(set-current-implementation :ndarray)

(defn lines [input-file]
  (split (slurp input-file) #"\n"))

(defn as-long [s] (Long/parseLong s))

(defn destruct [s]
  (let [[all & params] (re-find #"#(\S+) @ (\d+),(\d+): (\d+)x(\d+)" s)]
    (when all (map as-long params))))

(defn star
  (let [ll (lines input-file)
        field (new-matrix 1000 1000)]
    (loop [[[id col row w h] & xlines] (map destruct ll)]
      (if-not id
        (let [sm (submatrix field row h col w)]
          (emap! #(if (zero? %) id -1) sm)
          (recur xlines))))
    (ereduce + (eq field -1))))

This uses the same lines function as the last 2 days, and as-long is a trivial wrapping of Java interop so I can map it over the numbers found in each line. I could have just mapped an anonymous function, but I just wish Clojure would include as-long/as-double by default, which is why I named it.

My parsing is far more effort than is needed. Someone else pointed out that the regular nature of the input meant that I could just have split the line with: (re-seq #"\d+" s)

I decided to leave mine alone, partly because it's what I came up with, and partly because it's defensive programming. This puzzle is fine, but in the real world my code will one day see a file containing data that breaks simplistic parsing. That's not saying that my code is solid: for instance, I never check if the rectangles are within the 1000x1000 boundaries, but I like to practice at least a little bit of defensive coding.

The loop is destructuring the parse results then iterating until it's done. Then comes the nice part of core.matrix: pulling out a submatrix (the current rectangle) and updating it. The final line uses the nice trick of using eq to represent booleans as 0/1 and adding them. I learnt to count up booleans that way via APL.

Part the Second

(defn star2
  (let [ll (lines input-file)
        field (new-matrix 1000 1000)]
    (loop [[[id col row w h] & xlines] (map destruct ll) ids #{}]
      (if-not id
        (first ids)
        (let [sm (submatrix field row h col w)
              ids' (ereduce #(if (zero? %2) %1 (disj %1 %2 id)) (conj ids id) sm)]
          (assign! sm id)
          (recur xlines ids'))))))

This part was actually easier, and ran significantly faster!

In an imperative language I would update the elements of the submatrix as I checked for overlaps. I could do that here too, but the code above is just much cleaner.

As it is, it keeps a set of the ids that currently don't overlap. The ereduce step first assumes that the current id won't overlap and adds it to the set. Then it checks if each cell has been written to, and if so it removes from the set the id of that cell, and the id that is being processed right now. Then the assign! updates the whole rectangle with the current id.

I liked using core.matrix here. It forced me to go through the docs to look for useful functions (like eq), and I also learnt some interesting gotchas with the library, which will be valuable to know.

code of conduct - report abuse