🎅🏻🎄 Day 4: Scratchcards ⭐⭐
Oggi mi prendo una gustosa (ma sofferta) rivincita sulla disfatta totale di ieri. La prima parte del problema non è stata particolarmente difficile, mentre la seconda mi ha richiesto un paio d’ore circa di tentativi. Credo però di aver imparato diverse cose sul linguaggio che sto usando. Perciò: qual è il problema?
Ci tocca dare una mano a un altro elfo che sembra non aver capito cosa potrebbe aver vinto da una pila di gratta-e-vinci (scratch-cards, in inglese). L’insieme dei (suoi) gratta-e-vinci è, per esempio, così:
Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
In ogni riga, sulla sinistra di |
ci sono i numeri vincenti, mentre sulla destra i nostri numeri. Senza sapere le regole, presumiamo che il punteggio sia calcolato a partire dai numeri vincenti.
- Il primo numero vincente che possediamo vale 1.
- Dal secondo numero in poi in nostro possesso, il punteggio raddoppia.
Per prima cosa, estraiamo i winning numbers che sono anche in nostro possesso:
winningNumbers = DeleteCases[
Apply[Intersection] /@
Flatten[StringCases[
SplitLines[day4], _ ~~ ": " ~~ wins__ ~~ "|" ~~ nums__ :>
StringSplit[{wins, nums}]], 1], {}]
C’è un parsing standard1 di ogni riga dell’input, in cui raccogliamo le due porzioni separate da |
. Poi, otteniamo i singoli numeri per i due gruppi con StringSplit[{wins, nums}]
.
Dopo aver “normalizzato” l’input (ci sbarazziamo di un livello di parentesi con Flatten
), prendiamo l’intersezione dei due gruppi per ogni riga. Infine, eliminiamo le liste vuote: delle carte 5 e 6 non abbiamo nessun numero vincente, quindi l’intersezione è l’insieme vuoto.
Ora non ci resta che calcolare i punteggi per ogni riga e fare la somma:
Total[Nest[2#&, 1, Length@# - 1] & /@ winningNumbers]
Nest
è un built-in utilissimo del Wolfram Language, uno dei costrutti che si usano al posto di loop espliciti. Nest[f, x, N]
applica ripetutamente f
per N
volte, partendo dal valore di partenza x
. Il numero di iterazioni è la lunghezza della lista meno 1 perché il raddoppio si applica dal secondo numero vincente in poi.
Part 2
Le regole corrette sono più complicate, e ce lo aspettavamo. Dobbiamo sempre determinare quanti dei numeri vincenti possediamo, ma questo numero ci dice un’altra cosa: a quante copie extra di carte abbiamo diritto.
E quali carte extra, di preciso? Le carte seguenti a quella corrente. Ad esempio: la carta 1 ha 4 numeri vincenti, quindi otteniamo una copia di ognuna delle 4 carte successive alla carta 1, dalla 2 alla 5.
Sappiamo due cose che rimangono costanti:
- I numeri vincenti per ogni carta
- Quali copie ricevo per ogni carta
A prima vista sembra un problema ricorsivo, ma non lo è. Bisogna solo capire che cosa dobbiamo aggiornare a ogni step (ossia a ogni carta) e come. Il “cosa” è facile: un contatore di quante carte 1, 2, ecc. abbiamo.
Il contatore parte da 1 per ogni carta:
counts = Counts@Range@Length@SplitLines[day4];
Ci salviamo anche quanti sono i numeri vincenti per ogni carta. Non ci interessa più quali, ma solo quanti sono.
winningNumbers =
Map[Length,
Apply[Intersection] /@
Flatten[StringCases[
SplitLines[day4], _ ~~ ": " ~~ wins__ ~~ "|" ~~ nums__ :>
StringSplit[{wins, nums}]], 1]
]
E adesso la parte più interessante, quella su cui ho sbattuto la testa praticamente tutto il tempo che ho impiegato a risolvere il puzzle:
With[{numCards = Length@SplitLines[day4]},
Fold[
With[{
updates = #1[First@#2] * Counts@Range[First@#2 + 1, Total@#2]
},
Merge[{#1, updates}, Total]
]&,
counts,
Thread[{Range@numCards, winningNumbers}]
]
] // Total
Ci sono diversi pezzi interessanti qui.
Fold
è un altro built-in molto versatile, parente stretto di Nest
. Fa una cosa leggermente diversa:
Fold[f, 1, {a, b, c}]
(* f[f[f[1, a], b], c] *)
Applica ripetutamente f
con due argomenti: un valore iniziale (il secondo argomento) e il primo della lista; poi il risultato e il secondo elemento della lista; poi il terzo con il secondo risultato, ecc.
Il nostro valore di partenza è ovviamente counts
, mentre la lista da cui Fold
pescherà gli argomenti da passare alla funzione come secondo parametro è Thread[{Range@numCards, winningNumbers}]
. Per l’input di esempio sarebbe:
{{1, 4}, {2, 3}, {3, 2}, {4, 1}, {5, 0}, {6, 0}}
È semplicemente una lista di coppie; ogni coppia è il numero di una carta e quanti numeri vincenti possediamo per quella carta. Abbiamo bisogno di entrambi i numeri per sapere quante e quali carte extra riceviamo.
Queste due informazioni vengono usate nella funzione di Fold
:
With[{
updates = #1[First@#2] * Counts@Range[First@#2 + 1, Total@#2]},
Merge[{#1, updates}, Total]
]&
updates
è un’Association che ci dice quante carte 1, 2, ecc. abbiamo guadagnato. Merge
poi combina updates
con il primo argomento che viene passato alla funzione di Fold
, e cioè il valore corrente di counts.
È più semplice provare a eseguire il codice per capire cosa succede davvero.
Come esempio, provo a fare un unraveling a mano. Ci sono due simboli strani qui sotto: %
e %%
. Indicano semplicemente l’output precedente e quello ancora prima, rispettivamente. Se gli output fossero una lista, % = output[[-1]]
e %% = output[[-2]]
.
wins = {{1, 4}, {2, 3}, {3, 2}, {4, 1}, {5, 0}, {6, 0}}
start = <|1 -> 1, 2 -> 1, 3 -> 1, 4 -> 1, 5 -> 1, 6 -> 1|>
(* primo step *)
Counts@Range[1 + 1, 1 + 4] * start[1]
Merge[{start, %}, Total]
(* secondo step *)
Counts@Range[2 + 1, 2 + 2] * %[2]
Merge[{%%, %}, Total]
(* terzo step *)
Counts@Range[3 + 1, 3 + 2] * %[3]
Merge[{%%, %}, Total]
(* OUTPUT *)
(* inizio *)
<|1 -> 1, 2 -> 1, 3 -> 1, 4 -> 1, 5 -> 1, 6 -> 1|>
(* primo step *)
<|2 -> 1, 3 -> 1, 4 -> 1, 5 -> 1|>
<|1 -> 1, 2 -> 2, 3 -> 2, 4 -> 2, 5 -> 2, 6 -> 1|>
(* secondo step *)
<|3 -> 2, 4 -> 2|>
<|1 -> 1, 2 -> 2, 3 -> 4, 4 -> 4, 5 -> 2, 6 -> 1|>
(* terzo step *)
<|4 -> 4, 5 -> 4|>
<|1 -> 1, 2 -> 2, 3 -> 4, 4 -> 8, 5 -> 6, 6 -> 1|>
La chiave di tutto è proprio la moltiplicazione tra quante carte di un certo numero possiedo e quante ne devo aggiungere. Se, per esempio, ogni carta 2 mi dà diritto ad altre 2 carte, qualora avessi più di una carta 2, ognuna di queste mi farebbe guadagnare le 2 carte extra. Ed era proprio questo punto che sbagliavo nei primi tentativi.
-
C’è una funzione che userò talmente tante volte che ne ho definita una scorciatoia:
SplitLines[s]
non è built-in, ma è un alias diStringSplit[s, "\n"]
. ↩︎