Skip to content

Commit 2939e4c

Browse files
committed
Day 17, simple cleanup
1 parent 916e38e commit 2939e4c

File tree

4 files changed

+172
-12
lines changed

4 files changed

+172
-12
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ _Warning:_ I write long explanations. So... yeah.
2626
| 14 | [source](src/advent_2020_clojure/day14.clj) | [blog](docs/day14.md) |
2727
| 15 | [source](src/advent_2020_clojure/day15.clj) | [blog](docs/day15.md) |
2828
| 16 | [source](src/advent_2020_clojure/day16.clj) | [blog](docs/day16.md) |
29+
| 17 | [source](src/advent_2020_clojure/day17.clj) | [blog](docs/day17.md) |

docs/day17.md

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Day Sixteen: Ticket Translation
2+
3+
* [Problem statement](https://adventofcode.com/2020/day/16)
4+
* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day16.clj)
5+
6+
---
7+
8+
Today's problem was a matter of parsing a complex incoming string, and then doing a bunch of set manipulation, maps,
9+
and filters. To be honest, I didn't find this one very entertaining, so I'm just going to quickly run through what
10+
each function does, show how the shape of the data changes from function to function, and call it a day.
11+
12+
---
13+
14+
## Part 1
15+
16+
The `parse-input` function parses the incoming String into my state object. I leverage the `utils/split-blank-line-seq`
17+
function from a few problems ago, which converts the giant String into three components -- one for the rules, the
18+
"your ticket" section, and the "nearby tickets" section. Then it's a little regex and a whole lot of Integer parsing.
19+
The output shape is a map, that definitely could be simplified if I were so inclined.
20+
21+
```clojure
22+
; Output shape
23+
{:fields [["name" [low1 high1] [low2 high2]]]
24+
:my-ticket [1 2 3 4]
25+
:nearby-tickets [[1 2 3] [4 5 6] [7 8 9]]}
26+
```
27+
28+
The `invalid-fields` function looks at all of the fields and combines them into a single list of `[low high]` pairs.
29+
Then the `flatten` function fully flattens the nearby tickets into a single list of integer values, which we filter
30+
down using `not-any?` to return only the values that aren't within any of the ranges. The output shape is a simple
31+
list of integers, representing the invalid fields.
32+
33+
```clojure
34+
(defn invalid-fields [{:keys [fields nearby-tickets]}]
35+
(let [every-range (->> (map rest fields) (apply concat))]
36+
(->> (flatten nearby-tickets)
37+
(filter (fn [fld] (not-any? (fn [[x y]] (<= x fld y)) every-range))))))
38+
```
39+
40+
Then `part1` just adds together all invalid fields.
41+
42+
```clojure
43+
(defn part1 [input]
44+
(->> input parse-input invalid-fields (apply +)))
45+
```
46+
47+
---
48+
49+
## Part 2
50+
51+
Part 2 was a little trickier to solve, and my answer is a bit wordy. But I wanted to focus a bit more on transforming
52+
the data from one state to another, rather than doing everything inline as a giant list. So the solution is a bunch
53+
of small transformations.
54+
55+
The overall strategy was to rip apart the complexity of the ticket structure. So I wanted to make a map of sets. To
56+
start, every ticket field was mapped to the set of all possible field indices. Then we go through every nearby ticket,
57+
and for every field type in which the ticket field doesn't fit, remove that index from the field type's set of
58+
indices. By removing every rule violation, we should be left with only the field types and the indexes for which all
59+
tickets validate. So with that...
60+
61+
`valid-tickets-only` takes in the full state, and returns only the nearby tickets that are valid, meaning that they
62+
don't have any of the fields described previously in the `invalid-fields` function. This returns all of the remaining
63+
tickets in their normal form, so `[[1 2 3] [4 5 6] [7 8 9]]`.
64+
65+
```clojure
66+
(defn valid-tickets-only [{:keys [nearby-tickets] :as parsed}]
67+
(let [bad-fields (-> parsed invalid-fields set)]
68+
(filterv (fn [t] (not-any? #(bad-fields %) t))
69+
nearby-tickets)))
70+
```
71+
72+
The next function, `ticket-pairs`, rips out every `[idx v]` tuple across all of the nearby tickets, since I found that
73+
easier to reason with than keeping the original vector of vectors. So this function takes in the ticket list, of shape
74+
`[[1 2 3] [4 5 6] [7 8 9]]` and returns a simple `[[0 1] [1 2] [2 3] [0 4] [1 5] [2 6] [0 7] [1 8] [2 9]]` vector.
75+
76+
```clojure
77+
(defn ticket-pairs [tickets]
78+
(mapcat (fn [ticket] (map-indexed (fn [idx v] (vector idx v)) ticket))
79+
tickets))
80+
```
81+
82+
The first beefy function is `possible-indexes`, with its coordinating function `all-possible-indexes`.
83+
`possible-indexes` takes in the list of all `ticket-pairs` and the total number of fields in the tickets, and then
84+
a single `field` rule definition of form `["name" [low1 high1] [low2 high2]]` from problem state. We run a reducing
85+
function, starting with a set of all possible indexes, and then we look at every ticket pair. If that pair's value
86+
doesn't comply with the rule, remove that pair's index from the set of possible indexes. There's a whole bunch of
87+
extra calculations going on, but this algorithm is still very fast. As a reminder to my future self -- we use `dissoc`
88+
to remove a mapping from a map, but `disj` to remove a value from a set. Why one function can't do both...
89+
90+
Then `all-possible-fields` just maps each field to the set of possible indexes, and throws it all into a map.
91+
92+
```clojure
93+
(defn possible-indexes [ticket-pairs num-fields [name [low1 high1] [low2 high2]]]
94+
(->> (reduce (fn [acc [idx v]]
95+
(if (or (<= low1 v high1) (<= low2 v high2))
96+
acc
97+
(disj acc idx)))
98+
(set (range num-fields))
99+
ticket-pairs)
100+
(vector name)))
101+
102+
(defn all-possible-indexes [fields nearby-ticket-pairs]
103+
(->> fields
104+
(map #(possible-indexes nearby-ticket-pairs (count fields) %))
105+
(into {})))
106+
```
107+
Armed with the possible fields, our goal is to use `field-mappings` to actually decide which field belongs to which
108+
index. We use a simple `loop-recur`, starting with all fields being `unsolved`, and moving them one-by-one into the
109+
`solved` set. In each loop, we find one field that only has one index that is valid and hasn't been claimed yet by
110+
another field. Knowing the field name and its singular index, we use `remove-all-traces` to eliminate that index
111+
from all remaining fields' set of indexes, and loop again.
112+
113+
```clojure
114+
(defn remove-all-traces [possible-indexes index]
115+
(loop [fields possible-indexes, keys (keys possible-indexes)]
116+
(if-let [k (first keys)]
117+
(recur (update fields k #(disj % index))
118+
(rest keys))
119+
fields)))
120+
121+
(defn field-mappings [fields nearby-ticket-pairs]
122+
(loop [unsolved (all-possible-indexes fields nearby-ticket-pairs)
123+
solved {}]
124+
(if (empty? unsolved)
125+
solved
126+
(let [[name actual-index] (->> unsolved
127+
(keep (fn [[name indexes]]
128+
(when (= 1 (count indexes))
129+
[name (first indexes)])))
130+
first)]
131+
(recur (-> unsolved
132+
(dissoc name)
133+
(remove-all-traces actual-index))
134+
(assoc solved name actual-index))))))
135+
```
136+
137+
Finally, `part2` puts the pieces together. Starting with the input, we parse it, identify the valid tickets, flatten
138+
them into ticket pairs, and identify the mappings. With the correct mappings in-place, we use a `keep-when` to pick
139+
the fields whose name starts with `departure`, map each to the value that that index in `my-ticket`, and multiple the
140+
values together.
141+
142+
```clojure
143+
(defn part2 [input]
144+
(let [{:keys [fields my-ticket] :as parsed} (parse-input input)
145+
valid-tickets (valid-tickets-only parsed)
146+
nearby-ticket-pairs (ticket-pairs valid-tickets)
147+
mappings (field-mappings fields nearby-ticket-pairs)]
148+
(->> mappings
149+
(keep (fn [[name idx]]
150+
(when (str/starts-with? name "departure") (get my-ticket idx))))
151+
(apply *))))
152+
```
153+
154+
All in all, there's a bunch of code here, but it's all reasonably straightforward... now that it's done!

src/advent_2020_clojure/day17.clj

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
(ns advent-2020-clojure.day17
22
(:require [clojure.string :as str]))
33

4-
(defn neighbors-of [[x y z w] four-d?]
4+
(defn neighbors-of [four-d? [x y z w]]
55
(for [new-w (if four-d? [-1 0 1] [0])
66
new-x [-1 0 1]
77
new-y [-1 0 1]
88
new-z [-1 0 1]
99
:when (some #(not= 0 %) [new-x new-y new-z new-w])]
1010
[(+ x new-x) (+ y new-y) (+ z new-z) (+ w new-w)]))
11+
(def neighbors-3d (partial neighbors-of false))
12+
(def neighbors-4d (partial neighbors-of true))
1113

1214
(defn parse-input [input]
1315
(->> (str/split-lines input)
@@ -33,26 +35,29 @@
3335
w (inclusive-range cube-w)]
3436
[x y z w]))
3537

36-
(defn num-active-neighbors [active-points four-d? point]
37-
(->> (neighbors-of point four-d?)
38+
(defn num-active-neighbors [active-points neighbor-fn point]
39+
(->> (neighbor-fn point)
3840
(filter #(active-points %))
3941
count))
4042

41-
(defn next-point [active-points four-d? point]
42-
(let [actives (num-active-neighbors active-points four-d? point)]
43+
(defn next-point [active-points neighbor-fn point]
44+
(let [actives (num-active-neighbors active-points neighbor-fn point)]
4345
(if (active-points point)
4446
(contains? #{2 3} actives)
4547
(= 3 actives))))
4648

47-
(defn next-board [active-points four-d?]
49+
(defn next-board [active-points neighbor-fn]
4850
(->> active-points
4951
bounding-cube
5052
points-in-cube
51-
(keep #(when (next-point active-points four-d? %) %))
53+
(keep #(when (next-point active-points neighbor-fn %) %))
5254
set))
5355

54-
(defn part1 [input]
55-
(count (nth (iterate #(next-board % false) (parse-input input)) 6)))
56+
(defn solve [input neighbor-fn]
57+
(let [active-seq (->> input
58+
parse-input
59+
(iterate #(next-board % neighbor-fn)))]
60+
(count (nth active-seq 6))))
5661

57-
(defn part2 [input]
58-
(count (nth (iterate #(next-board % true) (parse-input input)) 6)))
62+
(defn part1 [input] (solve input neighbors-3d))
63+
(defn part2 [input] (solve input neighbors-4d))

test/advent_2020_clojure/day17_test.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111

1212
(deftest part2-test
1313
(is (= 848 (part2 TEST_DATA)))
14-
(is (= -1 (part2 PUZZLE_DATA))))
14+
(is (= 2192 (part2 PUZZLE_DATA))))

0 commit comments

Comments
 (0)