Ho appena completato il giorno 6, la parte 1 molto semplice la 2 richiede qualche ottimizzazione su cui si può ragionare.
Per ricapitolare un pò il problema, si ha una mappa con degli ostacoli ed un personaggio che si muove sulla mappa con regole semplici.
La parte 1 chiede di trovare su quante caselle differenti entra il personaggio prima di lasciare la mappa (non ci sono cicli e di certo il personaggio esce dopo un po’).
Io l’ho implementato così:
Parte 1 in clojure
(def input
(->> "resources/day6.txt"
io/reader
line-seq
(mapv vec)))
(defn get-start-pos [data]
(reduce
#(when (= \^ (get-in data %2)) (reduced %2))
nil
(for [i (range (count data)) j (range (count (first data)))]
[i j])))
(def next-direction
{:north :east
:east :south
:south :west
:west :north})
(defn move [[x y] dir]
(case dir
:north [(dec x) y]
:east [x (inc y)]
:south [(inc x) y]
:west [x (dec y)]))
(defn seen [data]
(loop [pos (get-start-pos data) dir :north visited #{pos}]
(let [next (move pos dir) spot (get-in data next)]
(case spot
nil visited
\# (recur pos (next-direction dir) visited)
\^ (recur next dir (conj visited next))
\. (recur next dir (conj visited next))))))
(defn part-1 [data]
(count (seen data)))
(println (part-1 input))
Di base ho trasformato il testo di input in una matrice di caratteri per poter sfruttare l’accesso diretto in O(1), poi faccio muovere il personaggio secondo le regole date e mi salvo le posioni in un set, così si scartano automaticamente i duplicati.
Alla fine conto gli elementi nel set ed è fatta.
Per la seconda parte ho trovato un paio di ottimizzazioni carine, sono andato di brute force (si usa più di quanto ci si aspetti in queste competizioni) ma ho cercato di evitare quante più operazioni possibili.
Il problema ora chiede di trovare in quanti modi è possibile posizionare un singolo ostacolo per far andare in loop il personaggio sulla mappa.
Le ottimizzazioni le accenno nel blocco nascosto si seguito:
Parte 2 in clojure
(defn loop? [start start-dir board]
(loop [pos start
dir start-dir
seen #{}]
(let [pos' (move pos dir)
spot (get-in board pos')]
(if (seen [pos' dir])
true
(case spot
\# (recur pos (next-direction dir) (conj seen [pos dir]))
(\. \^) (recur pos' dir seen)
nil false)))))
(defn part-2 [data]
(let [path (seen data) start (get-start-pos data)
loop? (partial loop? start :north)
boards (for [p path :when (not= p start)] (assoc-in data p \#))]
(->> boards
(filter loop?)
count)))
(println (part-2 input))
La prima ottimizzazione è stata quella di non provare a posizionare l’ostacolo in ogni posizione ma solo in quelle in cui il personaggio passa effettivamente, dato che possiamo posizionare un solo blocco di sicuro deve stare su una delle caselle individuate nella parte 1.
Da qui prendo e genero tutte le possibili mappe (o come le ho chiamate nel codice boards) aggiungendo l’ostacolo sul percorso (evitando la casella di inizio perchè così vuole il problema).
Per ognuna di queste board faccio muovere il personaggio, se esce fuori dalla mappa scarto la board, altrimenti se entra in un loop la board è valida e quindi la conteggio, come al solito il problema vuole il numero non la configurazione.
Fino a qua tutto ok però c’è da notare che per capire se sono in un ciclo servono informazioni su posizione e direzione, non basta sapere che sono già passato in una casella, ma ci devo passare due volte con la medesima direzione per entrare in un ciclo.
Per dire se passo per la cella (2, 3) andando in direzione nord allora non genero un ciclo se ci ripasso mentre sono diretto vero ovest:
A B
's' := starting point
'<' or '^' := movement direction
+--------s s--<--+
| ^ | |
| | | |
+--------+ +-----+
Spero si capisca, la mia preview è leggermente "sminchiata"
In questo caso A non genera un ciclo (sempre considerando le regole di movimento del personaggio) perchè possiamo proseguire dritto:
A
^
|
+--------s
| |
| |
+--------+
Mentre in B entriamo di sicuro in un ciclo.
Per tornare all’ottimizzazione salvare tutte le terne (x y direction)
è un pò costoso quindi ho cercato di capire se potevo evitarmene un paio.
Quello che ho notato è che per entrare in un ciclo devo prima incontrare l’ostacolo e poi ruotare il personaggio nella direzione in cui inizia il ciclo, quindi anzichè salvarmi tutte le terne (x y dir)
posso restringere il campo alle (x y dir)
tali che board[x][y]
siano un ostacolo.
Se incontro un ostacolo per la seconda volta allora sono in un loop.
Con questa ottimizzazione la mia ram mi ha ringraziato non poco, questa era la fonte di overhead più grande di tutte dato che c’erano un sacco di “copie” fatte in background per mantenere l’immutabilità (e mi sono rifiutato di usare strutture dati mutabili).
Ultima ottimizzazzione, make threads go brr.
Dato che le boards sono generate in modo indipendente e possono essere verificate in modo indipendente le posso controllare in parallelo! (nel mio caso sono sulle 5000).
Dato che è tutto immutabile la soluzione è stata banale in clojure
(defn part-2-parallel [data]
(let [path (path data) start (get-start-pos data)
loop? (partial loop? start :north)
boards (for [p path :when (not= p start)] (assoc-in data p \#))]
(->> boards
(pmap loop?)
(filter true?)
count)))
(part-2-parallel input)
Lanciato su 16 thread ha trovato la soluzione in meno della metà del tempo, quindi presumo che abbia uno speedup ed efficenza non siano proprio il massimo, probabilmente 16 thread sono troppi.